A GraphQL field resolver called once per parent can turn one page request into N database queries; @BatchMapping groups those parent keys for one fetch.
Spring GraphQL @BatchMapping: collapse N+1 lookups without changing field behavior
Count database calls
A query returns 47 parcel records and asks for each parcel's depot. A naïve @SchemaMapping loads one depot for every parcel, making 48 database calls instead of a bounded batch. @BatchMapping receives the parents as a list and can fetch their depot IDs in one query, returning a map from each original parent to its depot. Page bounds still matter: batching 47 rows is manageable, batching 47,000 can exhaust memory or exceed a database parameter limit.
Preserve missing-value semantics
The batch map may omit a parent whose depot was removed. Decide whether the schema field is nullable and whether omission becomes null or a field error. Non-null fields can cause error propagation into the parent response. Use stable equality for parent keys if they are map keys; a mutable entity whose hash changes while loading can lose its result.
Verify the query shape
Request 47 parcels with depots, assert the result IDs match their parents, and measure query count. Repeat with a missing depot and a page boundary. The method below is a mapping sketch; the repository method must include tenant scope and a bounded IN query before it is safe for production.
Implementation sketch
@BatchMapping(typeName = "Parcel", field = "depot")
Map<Parcel, Depot> depot(List<Parcel> parcels, Principal principal) {
UUID tenantId = tenantResolver.requireTenant(principal);
Set<Long> depotIds = parcels.stream()
.map(Parcel::depotId).collect(Collectors.toSet());
Map<Long, Depot> depots = depotRepository
.findByTenantIdAndIdIn(tenantId, depotIds).stream()
.collect(Collectors.toMap(Depot::id, Function.identity()));
return parcels.stream().filter(parcel -> depots.containsKey(parcel.depotId()))
.collect(Collectors.toMap(Function.identity(),
parcel -> depots.get(parcel.depotId())));
}Cost and verification
One batched lookup lowers round trips but can create a large IN query and a map proportional to page size. Bound the parent list and measure SQL plan changes.
Common Mistakes
- Do not claim N+1 is fixed if the repository still loops per ID.
- Do not use mutable parent keys without stable equals and hashCode.
- Do not ignore how a missing related row interacts with schema nullability.
Read next
Spring GraphQL batch loader tenant scope: never key a shared cache by row ID alone, Spring GraphQL list bounds: reject large arguments before reading rows, Spring GraphQL schema nullability: trace one field failure through the response, Spring JPA fetch plans: measure N+1 queries before changing mappings.
