Batch Requests

Learn how to efficiently batch operations with Connectors

Requires ≥ Federation v2.11, Router v2.3

The "N+1 problem" is a common challenge in GraphQL APIs that can lead to performance issues. You start with an initial request (1), that returns a list of objects. The size of the list determines how many follow-up requests (N) are necessary. The exact number of follow-up requests is not known until the first request is executed.

Consider a simple e-commerce API that provides a list of reviews, each with a rating and a reference to a product. The product is an entity that can be fetched separately.

GraphQL
1type Query {
2  reviews: [Review] # A list!
3    @connect(
4      source: "ecom"
5      http: { GET: "/reviews" }
6      selection: "id rating product: { id: productId }"
7    )
8}
9
10type Review {
11  id: ID!
12  rating: Int!
13  product: Product
14}
15
16type Product
17  @connect(
18    source: "ecom"
19    http: { GET: "/product/{$this.id}" }
20    selection: "id name"
21  ) @key(fields: "id") {
22  id: ID!
23  name: String
24}

In the Connectors Debugger, you can see that fetching the list of reviews results in a separate request for each product associated with the review.

Connectors debugger screenshot showing one request per product

To solve the N+1 problem, you can use Connectors batching. With batching, a single request is made for all N items in the list.

GraphQL
1type Product
2  @connect(
3    source: "ecom"
4    # Use the $batch variable to make a batch request for products
5    http: { GET: "/product", queryParams: "id: $batch.id" }
6    selection: "id name"
7  ) {
8  id: ID!
9  name: String
10}

This reduces the total number of requests to two: one for the list of reviews and one for the list of products.

Connectors debugger screenshot showing a batch request for products
note
Learn more about the similarities between Connectors batching and DataLoaders in the N+1 handling guide.

Fetching entities in batches

Using Connectors batching requires an HTTP endpoint that accepts a list of keys and returns a list of items. This is often referred to as a batch endpoint. There are a few common patterns for batch endpoints and how to use the Connectors $batch variable to make batch requests:

Repeated query parameters

Using repeated query parameters, each ID is sent as a separate query parameter in the HTTP request, for example, ?id=1&id=2.

Use the http.queryParams mapping to specify the query parameter name and the value from $batch.id. The $batch.id variable is a list of IDs that will be sent in the request.

GraphQL
http: { GET: "/" queryParams: "id: $batch.id" }

Comma-separated list

Using a comma-separated list, all IDs are passed in a single query parameter, for example, ?ids=1,2. Commas will be percent-encoded: ?ids=1%2C2.

Use the joinNotNull mapping method to join the IDs into a single string, ignoring any null values. Interpolate the $batch.id variable in the http.GET mapping to create the query parameter.

GraphQL
http: { GET: "?ids={$batch.id->joinNotNull(',')}" }
note
The ->joinNotNull mapping method creates a string from a list of scalar values by joining them with a separator. It ignores null values, which is important when dealing with entity references from the query planner. Read more about the implications of null entity references.

JSON request body

Using a JSON request body, all IDs are passed in as a filter criteria in the JSON payload, for example, { "ids": [1, 2] }.

Use the http.body mapping to specify the request body. The $batch.id variable is used to access the list of IDs.

GraphQL
http: { POST: "/products" body: "ids: $batch.id" }

Composite keys

In this example, the Product type has a composite key that includes the id and the store.id. The request body needs to include both fields to uniquely identify each product.

JSON
Example request body
1{
2  "items": [
3    { "id": 1, "store_id": 10 },
4    { "id": 2, "store_id": 20 },
5    { "id": 3, "store_id": 10 }
6  ]
7}
GraphQL
1type Product
2  @connect(
3    source: "ecom"
4    http: {
5      POST: "/products"
6      body: """
7      items: $batch { 
8        id 
9        store_id: store.id 
10      }
11      """
12    }
13    selection: """
14    id
15    name
16    store { id name }
17    """
18  ) {
19  id: ID!
20  name: String
21  store: Store
22}
23
24type Store {
25  id: ID!
26  name: String
27}

Rules for batch Connectors

  • The mappings from the $batch variable must be scalar fields of the entity type.

    ✅ Valid example
    GraphQL
    1type Product
    2  @connect(
    3    source: "ecom"
    4    http: {
    5      GET: "/products"
    6      queryParams: "id: $batch.id"
    7    }
    8    selection: """
    9    id
    10    name
    11    """
    12  ) {
    13  id: ID! # scalar field
    14  name: String
    15}
    ❌ Invalid example
    GraphQL
    1type Product
    2  @connect(
    3    source: "ecom"
    4    http: {
    5      GET: "/products"
    6      body: "id: $batch.variation"
    7    }
    8    selection: """
    9    id
    10    name
    11    """
    12  ) {
    13  id: ID!
    14  name: String
    15  variation: Variation
    16}
    17
    18type Variation {
    19  id: ID!
    20  color: String
    21}
  • The selection mapping for a batch request must evaluate to a list of objects.

    ✅ Valid example
    GraphQL
    1type Product
    2  @connect(
    3    source: "ecom"
    4    http: {
    5      GET: "/products"
    6      queryParams: "id: $batch.id"
    7    }
    8    selection: """
    9    # unwrap the { "results": [...] } wrapper
    10    $.results {
    11      id
    12      name
    13    }
    14    """
    15  ) {
    16  id: ID!
    17  name: String
    18}
    ❌ Invalid example
    GraphQL
    1type Product
    2  @connect(
    3    source: "ecom"
    4    http: {
    5      GET: "/products"
    6      queryParams: "id: $batch.id"
    7    }
    8    selection: """
    9    # evaluates to { "results": [{ "id": 1, "name": "Hat" }] }
    10    results {
    11      id
    12      name
    13    }
    14    """
    15  ) {
    16  id: ID!
    17  name: String
    18}
  • The selection mapping for a batch request must contain the same fields as the $batch variable.

    ✅ Valid example
    GraphQL
    1type Product
    2  @connect(
    3    source: "ecom"
    4    http: {
    5      GET: "/products"
    6      queryParams: "id: $batch.id"
    7    }
    8    selection: """
    9    $.results {
    10      # maps the product_id field to the id field referenced from $batch
    11      id: product_id
    12      name
    13    }
    14    """
    15  ) {
    16  id: ID!
    17  name: String
    18}
    ❌ Invalid example
    Not selecting the id field means we can't associate the response items to the keys in the batch.
    GraphQL
    1type Product
    2  @connect(
    3    source: "ecom"
    4    http: {
    5      GET: "/products"
    6      queryParams: "id: $batch.id"
    7    }
    8    selection: """
    9    $.results {
    10      name
    11    }
    12    """
    13  )
    14{
    15  id: ID!
    16  name: String
    17}

Batch options

maxSize

You may want to limit the number of items included in each batch request. This is useful when the API has a limit on the number of items that can be fetched in a single request.

Use the batch.maxSize option to specify the maximum number of items included in each batch request. This results in (N / maxSize) + 1 requests to your API.

GraphQL
1type Product
2  @connect(
3    source: "ecom"
4    http: { GET: "/products", queryParams: "id: $batch.id" }
5    selection: "id name"
6    batch: { maxSize: 10 }
7  ) {
8  id: ID!
9  name: String
10}

A complex batching example

The Fast Healthcare Interoperability Resources (FHIR) standard defines a RESTful API for exchanging healthcare data. It has an unusual pattern for batch requests that takes a little more effort to implement with Connectors.

Batch requests in FHIR are made by sending a JSON payload with a list of requests. Each request has a method, URL, and optional resource.

JSON
1{
2  "resourceType": "Bundle",
3  "type": "batch",
4  "entry": [
5    {
6      "request": {
7        "method": "GET",
8        "url": "Patient/123"
9      }
10    },
11    {
12      "request": {
13        "method": "GET",
14        "url": "Patient/456"
15      }
16    },
17    {
18      "request": {
19        "method": "GET",
20        "url": "Patient/789"
21      }
22    }
23  ]
24}

You can accomplish this with Connectors by using the http.body mapping to create the JSON payload. The $batch variable is used to access the list of IDs and construct the requests.

GraphQL
1@connect(
2  http: {
3    POST: "https://fhir.example.com/"
4    body: """
5    resourceType: $("Bundle")
6    type: $("batch")
7    entry: $batch.id { # Iterate over the batch ids
8      request: {
9        method: $("GET")
10        url: $(["Patient", $])->joinNotNull("/")
11      }
12    }
13    """
14  }
15)

Lastly, you can map over resources in the response to return a list that the query planner can use to resolve the entities in the batch.

GraphQL
1@connect(
2  selection: """
3  $.entry.resource {
4    id
5    name {
6      family
7      given
8    }
9  }
10  """
11)
Connectors Mapping Playground
Best viewed on a larger screen

Limitations of "has-many" relationships

With a relationship like Product.reviews, it's usually not possible to batch requests. These relationships often involve pagination and providing the parameters in a batch request isn't possible.

In this example, fetching the reviews for a list of products will always result in a separate request for each product.

GraphQL
1type Product {
2  id: ID!
3  name: String
4  reviews(first: Int, after: String): [Review]
5    @connect(
6      source: "ecom"
7      http: {
8        GET: """
9        /products/{$this.id}/reviews
10        ?first={$args.first}
11        &after={$args.after}
12        """
13      }
14      selection: "id rating"
15    )
16}
Feedback

Ask Community