Catch the highlights of GraphQLConf 2023! Click for recordings. Or check out our recap blog post.
Handbook
Foundation
Array-batched type merging

Array-batched type merging

This example explores the core techniques for merging typed objects using array queries, covering most of the topics discussed in batched merging documentation.

This example focuses on array batching—meaning that all records accessed during a round of delegation are batched together and loaded as an array. This technique greatly reduces the execution overhead of single-record merges, and can be further optimized by enabling query batching. This array-batched strategy is prefereable to single-record merges and should be used whenever possible.

This example demonstrates:

Related examples:

Sandbox

⬇️ Click ☰ to see the files

You can also see the project on GitHub here.

The following services are available for interactive queries:

  • Stitched gateway: listening on 4000/graphql
  • Manufacturers subservice: listening on 4001/graphql
  • Products subservice: listening on 4002/graphql
  • Storefronts subservice: listening on 4003/graphql

Summary

Visit the stitched gateway and try running the following query:

query {
  storefront(id: "2") {
    id
    name
    products {
      upc
      name
      manufacturer {
        products {
          upc
          name
        }
        name
      }
    }
  }
}

If you study the results of this query, the final composition traverses across the service graph:

  • Storefront (Storefronts schema)
    • Storefront.products -> Product (Products schema)
      • Product.manufacturer -> Manufacturer (Products + Manufacturers schemas)
        • Manufacturer.products (Products schema)
        • Manufacturer.name (Manufacturers schema)

That means the gateway performed three rounds of resolution for each service's data (Services -> Products -> Manufacturers). With the array-batching technique, the gateway only performs a single delegation per round, regardless of the number of records in the round.

Error handling

Pay special attention to the error handling used while resolving record arrays:

manufacturers(root, { ids }) {
  return ids.map(id => manufacturers.find(m => m.id === id) || new NotFoundError());
}

It is extremely important that errors get mapped into the result set, rather than being thrown (which corrupts the entire result set).

Nullability + mapped errors

Also run a query for the Product with UPC "6", and you'll see an interesting feature of error handling:

query {
  products(upcs: ["6"]) {
    upc
    name
    manufacturer {
      name
    }
  }
}

For the purposes of this example, this product intentionally specifies an invalid manufacturer reference. You'll see that the original error from the underlying subservice has flowed through the stitching process and is mapped to its final document position in the stitched schema:

{
  "errors": [
    {
      "message": "Record not found",
      "locations": [],
      "path": ["products", 0, "manufacturer"],
      "extensions": {
        "code": "NOT_FOUND"
      }
    }
  ],
  "data": {
    "products": [
      {
        "upc": "6",
        "name": "Baseball Glove",
        "manufacturer": null
      }
    ]
  }
}

Note that for this process to work, the Product.manufacturer reference must be nullable, otherwise you'll get a GraphQL nullability-mismatch error when the manufacturer returns an error. For this reason, it's generally best-practice to make all stitched associations nullable on the assumption that the record association could fail, at which time it's better to see the subschema failure than a top-level GraphQL nullability error.