Batch Resolution

Batch node and field resolvers

Both node resolvers and field resolvers can be implemented using the batchResolve function. This provides an alternative to the widely used data loader pattern.

The N+1 problem

Consider this example schema:

type Query {
  recommendedListings: [Listing] @resolver
}

type Listing implements Node {
  id: ID!
  title: String
}

Suppose the query below returns 3 recommended listings. A Listing node resolver that makes a call to a listings service to fetch a single listing in the resolve function will result in 3 separate calls to the service.

query {
  recommendedListings {
    id
    title
  }
}

This is the N+1 problem, which is commonly solved by implementing a data loader that batches calls to the listings service. The resolver calls the data loader, which then calls the data source.

batchResolve

In Viaduct, you can implement the batchResolve function and directly call the data source instead of going through a data loader. Under the hood, Viaduct still uses a data loader to batch requests. However, this data loader is part of Viaduct’s framework, not something that application developers need to write and maintain. Here’s an example Listing batch node resolver:

@Resolver
class ListingNodeResolver @Inject constructor(val client: ListingClient) : NodeResolvers.Listing() {
  override suspend fun batchResolve(contexts: List<Context>): List<FieldValue<Listing>> {
    val listingIDs = contexts.map { it.id.internalID }
    val responses = client.fetch(listingIDs)

    return contexts.map { ctx ->
      val listingID = ctx.id.internalID
      val response = responses[listingID]
      FieldValue.ofValue(
        Listing.Builder(ctx)
          .title(response.title)
          .build()
      )
    }
  }
}

Input

batchResolve takes a list of Context objects as input. This is the same Context object type passed to the non-batching resolve function. Viaduct’s GraphQL execution engine batches these contexts before passing them to the batchResolve function.

Output

The list that batchResolve returns must have the same number of elements as the input list. Each output value corresponds to the input Context at the same list index.

FieldValue

Notice that the output list elements are wrapped in FieldValue (e.g., List<FieldValue<Listing>> in the example above). This represents either a successfully resolved value or an error value.

Usage:

  • FieldValue.ofValue(v): constructs a successfully resolved value, as shown in the example above
  • FieldValue.ofError(e): constructs an error value, where e is an exception. The corresponding value in the GraphQL response will be null, and there will be an error in the errors array.

When to use batchResolve

Override batchResolve whenever you need to fetch data from an external data source that supports batch loading. This solves the N+1 problem and similar issues where multiple parts of a GraphQL query fetch data that can be batched together.

If your resolver does not have external data dependencies, there is generally no benefit to implementing batchResolve.

Those familiar with data loaders may know that they also provide an intra-request cache. In Viaduct, this memoization cache is decoupled from batching, so you do not need to implement batchResolve for caching purposes.