Batch Field Resolvers
Batch field resolvers process multiple field requests in one pass, dramatically improving performance when the same field is selected across many parent objects. Viaduct guarantees the input order of contexts and expects you to return results in the same order.
Where batching fits in the execution flow
- The planner groups identical field selections across all matching parent objects in the operation.
- Viaduct calls your
batchResolve(contexts: List<Context>). - You perform one data fetch per unique key set (for example, character IDs).
- You map results back to each context and return a
List<FieldValue<T>>aligned with the input order.
Minimal example (counts per character)
@Resolver(objectValueFragment = "fragment _ on Character { id }")
class CharacterFilmCountResolver : CharacterResolvers.FilmCount() {
override suspend fun batchResolve(contexts: List<Context>): List<FieldValue<Int>> {
// Extract all unique character IDs from the contexts
val characterIds = contexts.map { it.objectValue.getId().internalID }.toSet()
// Perform a single batch query to get film counts for all characters
// We only compute one time for each character, despite multiple requests
val filmCounts = characterIds.associateWith { characterId ->
CharacterFilmsRepository.findFilmsByCharacterId(characterId).size
}
// For each context gets the character ID and map to the precomputed film count
// and return the results in the same order as contexts
return contexts.map { ctx ->
val characterId = ctx.objectValue.getId().internalID
FieldValue.ofValue(filmCounts[characterId] ?: 0)
}
}
Choosing the fragment
The objectValueFragment declares the parent fields your resolver needs. Keep it minimal — requesting only id
is typical for lookup scenarios. If you require additional, cheap fields (for example, name for formatting), add them
here so they are available on ctx.objectValue without extra work.
@Resolver(objectValueFragment = "fragment _ on Character { id }")
class CharacterFilmCountResolver : CharacterResolvers.FilmCount() {
Implementing batch resolvers in node resolvers
Node resolvers can also be batched. The pattern is similar, but you receive a list of GlobalIDs instead of
Contexts. You can use GlobalID.toInternalID() to extract your internal ID
override suspend fun batchResolve(contexts: List<Context>): List<FieldValue<Character>> {
// Extract all unique character IDs from the contexts
val characterIds = contexts.map { it.id.internalID }
// Perform a single batch query to get film counts for all characters
// We only compute one time for each character, despite multiple requests
val characters = characterIds.mapNotNull {
CharacterRepository.findById(it)
}
// For each context gets the character ID and map to the viaduct object
return contexts.map { ctx ->
val characterId = ctx.id.internalID
characters.firstOrNull { it.id == characterId }?.let {
FieldValue.ofValue(
CharacterBuilder(ctx).build(it)
)
} ?: FieldValue.ofError(IllegalArgumentException("Character not found: $characterId"))
}
}
}
For a node resolver you can only implement
batchResolveorresolve— not both.
Error handling and nullability
- Return a sensible default or
FieldValue.ofNull()for missing items (match schema nullability). - Avoid throwing for “not found” cases — reserve exceptions for unexpected failures.
- Ensure the size of the returned list matches
contexts.sizeexactly.
When to batch (and when not to)
Batch when:
- The same field is selected for many parent objects in a single operation.
- The data access layer supports bulk retrieval by keys (IDs).
- You would otherwise repeat the same lookup per parent (N + 1 pattern).
Prefer single resolvers when:
- Only a handful of parents are involved.
- The logic is strictly local and cheap for each parent.
Example query that benefits from batching
Schema definition:
allCharacters(limit: Int): [Character]
@resolver
@backingData(class: "com.example.starwars.modules.filmography.characters.queries.AllCharactersQueryResolver")
Executed query:
query {
allCharacters(limit: 100) {
filmCount # resolved by FilmCountBatchResolver in one grouped call
}
}
Do and don’t
- Do request only the parent fields you need in
objectValueFragment. - Do deduplicate keys before hitting the data layer.
- Do return results in the same order as the input contexts.
- Don’t perform per-context DB calls inside
batchResolve. - Don’t allocate large intermediate structures unnecessarily — map directly back to contexts.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.