Batch loading reduces queries, but a loader that drops tenant identity or uses a cross-request cache can return another tenant's row.
Spring GraphQL batch loader tenant scope: never key a shared cache by row ID alone
Carry authorization into the batch
A GraphQL request contains parcel IDs 47 and 52. The batch fetch must still apply the authenticated tenant predicate; selecting by ID alone is a data leak even if the top-level parcel list was filtered. Spring's BatchLoaderRegistry creates DataLoaders per request and gives access to GraphQL context. That per-request cache is useful, but a separate application-wide cache keyed only by depot ID can cross tenant boundaries. Top-level authorization does not automatically secure nested data fetchers.
Choose the cache key
If depot IDs are tenant-local, include tenant ID in every cache and repository key. If IDs are globally unique, still enforce authorization at read time; uniqueness is not permission. A batch that merges parents from different GraphQL fields can carry distinct field arguments, so a simple @BatchMapping is insufficient when the nested field's filter changes by parent. Use the registry's context-aware loader in that case.
Test a collision
Create tenant A and tenant B with the same local depot number, query each as a separate principal, and verify the nested result stays within its tenant. Reverse request order to catch stale cache reuse. The repository call below shows the predicate; the surrounding loader and security context need an integration test.
Implementation sketch
Map<Long, Depot> loadDepots(UUID tenantId, Set<Long> depotIds) {
return depotRepository.findByTenantIdAndIdIn(tenantId, depotIds)
.stream().collect(Collectors.toMap(Depot::id, Function.identity()));
}Cost and verification
Tenant predicates add an index requirement on tenant ID plus row ID. Per-request caches cap data lifetime but still grow with query breadth; apply field and page limits.
Common Mistakes
- Do not infer nested-field authorization from the root resolver's check.
- Do not reuse a DataLoader cache across requests without tenant-aware keys.
- Do not batch across differing field arguments as if every parent had the same filter.
Read next
Spring GraphQL @BatchMapping: collapse N+1 lookups without changing field behavior, Spring GraphQL tenant reads: scope every resolver before returning a record, Spring JdbcTemplate tenant predicates: put ownership in the SQL query, Spring method authorization: reject a cross-tenant read.
