Spring Boot: Data Loading & Integration

This section documents the current GraphQL specification (October 2021), plus the working draft for @defer/@stream/@oneOf, and the GraphQL-over-HTTP specification, as published at graphql.org and spec.graphql.org — which are the references these pages are written and verified against — and, for the integration pages, against Spring for GraphQL (2.0.x), Strawberry, Ariadne, and the client docs for Apollo Client / urql / Relay. No specific patch version is pinned. Some surfaces (Apollo Router/managed federation, graphql-ws internals, the Relay compiler internals, GraalVM native) are linked rather than documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as GraphQL tooling iterates quickly.

This page covers the JVM-specific machinery Spring for GraphQL adds on top of the language-agnostic execution model: batching resolver calls with @BatchMapping/BatchLoaderRegistry, resolving federated entities with @EntityMapping, and auto-registering data fetchers straight from Spring Data repositories.

Why this needs a Spring-specific fix

Execution & Resolvers explains why nested fields are resolved independently, and Performance & N+1 covers the generic DataLoader pattern — batch and cache — that fixes the resulting round-trip explosion. Spring for GraphQL gives that pattern two JVM entry points: @BatchMapping, a declarative shortcut for the common case, and BatchLoaderRegistry, the lower-level API @BatchMapping itself is built on and the one to reach for when a batch function needs arguments, filtering, or other context the shortcut cannot express.

@BatchMapping: the declarative shortcut

A @BatchMapping method receives the list of parent objects already resolved for the current tick, instead of resolving its field once per parent. Spring for GraphQL correlates the returned values back to each parent automatically:

@Controller
public class BookController {

    private final AuthorRepository authorRepository;

    public BookController(AuthorRepository authorRepository) {
        this.authorRepository = authorRepository;
    }

    @BatchMapping
    public Mono<Map<Book, Author>> author(List<Book> books) {
        List<Long> authorIds = books.stream().map(Book::authorId).distinct().toList();
        return Flux.fromIterable(authorRepository.findAllById(authorIds))
                .collectMap(Author::id)
                .map(byId -> books.stream()
                        .collect(Collectors.toMap(book -> book, book -> byId.get(book.authorId()))));
    }
}

This registers a batch loading function for the Book.author field: Book instances are the keys, Author instances the values, and one DataFetcher is transparently bound to that field for every query that selects it. Beyond the List<K> of parents, a @BatchMapping method may also accept java.security.Principal, @ContextValue, the raw GraphQLContext, or BatchLoaderEnvironment. The return type is equally flexible: Mono<Map<K,V>> and Flux<V> for reactive implementations (a Flux result is matched back to parents by position), or the imperative Map<K,V>/Collection<V> and their Callable-wrapped async variants for non-reactive code — Execution & Resolvers covers the same sync/async duality for ordinary resolvers. Link: Spring for GraphQL — Batch Mapping.

Registering batch loaders directly with BatchLoaderRegistry

@BatchMapping is a shortcut over BatchLoaderRegistry, a bean the Boot starter declares automatically and that any configuration class or controller can inject to register a batch function for a key/value type pair directly — the option to reach for when the field needs an @Argument, or when a controller method wants to call dataLoader.load(id) itself instead of letting Spring bind the field automatically:

@Configuration
public class DataLoaderConfig {

    public DataLoaderConfig(BatchLoaderRegistry registry) {
        registry.forTypePair(Long.class, Author.class)
                .registerMappedBatchLoader((authorIds, env) ->
                        Flux.fromIterable(authorRepository.findAllById(authorIds))
                                .collectMap(Author::id));
    }
}

@Controller
public class BookController {

    @SchemaMapping
    public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> authorLoader) {
        return authorLoader.load(book.authorId());
    }
}

By default the DataLoader name matches the target entity’s simple class name, which is why the @SchemaMapping method above can declare a generically typed DataLoader argument without naming it explicitly. BatchLoaderRegistry also re-creates every registration once per request — so a batch function never leaks state across requests — and propagates the same GraphQLContext to both the batch function and any @SchemaMapping/@BatchMapping method reading it, which is the main reason applications register batch loaders through it rather than building a raw DataLoaderRegistry by hand. Link: Spring for GraphQL — BatchLoaderRegistry.

The diagram below is specific to Spring’s per-request wiring — contrast it with the generic batch-and-cache picture in Performance & N+1, which shows the same dispatch behavior without any framework attached to it:

sequenceDiagram participant Req as Incoming request participant Reg as BatchLoaderRegistry participant DL as Per-request DataLoaderRegistry participant R1 as Book 1 resolver participant R2 as Book 2 resolver participant Fn as Registered batch function Req->>Reg: request begins Reg->>DL: create fresh DataLoader instances for this request R1->>DL: authorLoader.load(1) R2->>DL: authorLoader.load(2) DL->>Fn: dispatch queued keys [1, 2] on next tick Fn-->>DL: Map of id -> Author DL-->>R1: Author 1 DL-->>R2: Author 2

@EntityMapping: resolving federated entity references

A federation gateway resolves a type owned by another subgraph through an _entities query carrying a "representation" — the type’s @key fields — for each instance it needs. @EntityMapping methods are how a Spring for GraphQL subgraph answers that query for its own owned types:

@Controller
public class BookController {

    private final BookRepository bookRepository;

    public BookController(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @EntityMapping
    public Book book(@Argument Long id) {
        return bookRepository.findById(id).orElse(null);
    }

    @EntityMapping
    public List<Book> book(@Argument List<Long> idList, DataLoader<Long, Book> bookLoader) {
        return idList.stream().map(bookLoader::load).map(CompletableFuture::join).toList();
    }

    @SchemaMapping
    public Author author(Book book) {
        return book.author();
    }
}

Each @Argument resolves one field out of the representation map — or a method may take the full Map<String, Object> directly for a composite key — and the List-typed overload above is the batch variant gateways use when a single _entities query asks for several instances of the same type at once, exactly the same batching shape @BatchMapping uses for ordinary fields. This page only covers the resolver-side signature; schema declaration, @key directives, and gateway composition belong to Federation. Link: Spring for GraphQL — Entity Mapping.

Spring Data integration: auto-registered data fetchers

Beyond hand-written controllers, Spring for GraphQL can wire a DataFetcher straight from a Spring Data repository, provided the repository is annotated @GraphQlRepository and implements QuerydslPredicateExecutor or QueryByExampleExecutor. Auto-registration only fills in a field that has no existing DataFetcher, matching the repository’s domain type simple name to the schema type name by default (override it with typeName when they differ), and it covers single-value, list, and paginated query shapes alike:

@GraphQlRepository
public interface AuthorRepository extends JpaRepository<Author, Long>,
        QuerydslPredicateExecutor<Author> {
}

Spring Data JPA covers what a Spring Data repository interface is and how its derived queries, Specification, and QuerydslPredicateExecutor support work; the rest of this section only covers the part specific to auto-registering that repository as a GraphQL data fetcher.

QuerydslDataFetcher

For a QuerydslPredicateExecutor repository, Spring for GraphQL derives a Predicate from the query’s arguments using the generated Q* metamodel type, the same one Spring Data JPA introduces for dynamic Criteria-free queries, and a QuerydslBinderCustomizer declared on the repository itself is picked up automatically to customize how individual arguments bind to predicate operators:

public interface AuthorRepository extends Repository<Author, Long>,
        QuerydslPredicateExecutor<Author>, QuerydslBinderCustomizer<QAuthor> {

    @Override
    default void customize(QuerydslBindings bindings, QAuthor author) {
        bindings.bind(author.name).first((path, value) -> path.containsIgnoreCase(value));
    }
}

QueryByExampleDataFetcher

For a QueryByExampleExecutor repository, the same auto-registration builds an Example<T> from the query’s arguments instead of a Predicate, matching only the non-null properties a client actually supplied — useful when the schema’s field set already mirrors the entity closely enough that a generated Q* type would be overkill:

public interface AuthorRepository extends Repository<Author, Long>,
        QueryByExampleExecutor<Author> {
}

Pagination: Window, Slice, and keyset scrolling

Pagination documents the Relay Cursor Connections shape a schema exposes to clients; on the resolver side, a Spring for GraphQL controller method binds that shape to a ScrollSubrange argument and returns a Spring Data Window or Slice, which built-in ConnectionAdapter implementations translate into Connection/Edge/PageInfo values automatically:

@Configuration
public class GraphQlConfig {

    @Bean
    public GraphQlSourceBuilderCustomizer connectionCustomizer() {
        CursorStrategy<ScrollPosition> strategy =
                CursorStrategy.withEncoder(new ScrollPositionCursorStrategy(), CursorEncoder.base64());
        GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(
                new WindowConnectionAdapter(strategy),
                new SliceConnectionAdapter(strategy)));
        return builder -> builder.typeVisitors(List.of(visitor));
    }
}

@Controller
public class BookController {

    @QueryMapping
    public Window<Book> books(ScrollSubrange subrange, Sort sort) {
        ScrollPosition position = subrange.position().orElse(ScrollPosition.keyset());
        Limit limit = subrange.count().map(Limit::of).orElse(Limit.unlimited());
        return bookRepository.findBy(sort, position, limit);
    }
}

ScrollSubrange carries the decoded forward/backward cursor as an offset- or keyset-based ScrollPosition plus the requested page size, mirroring the first/after/last/before arguments Pagination documents at the schema level. Link: Spring for GraphQL — Scroll.

Keyset cursors

With KeysetScrollPosition, the cursor encodes a keyset — a Map<String, Object> of the sort-key values identifying the last row seen — rather than a numeric offset, which keeps pagination stable under concurrent inserts and deletes the same way offset pagination cannot. The default JsonKeysetCursorStrategy serializes that keyset to JSON but needs an explicit allow-list to restore anything beyond basic types safely:

@Bean
public JsonKeysetCursorStrategy keysetCursorStrategy() {
    PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
            .allowIfBaseType(Map.class)
            .allowIfSubType(UUID.class)
            .allowIfSubType(Number.class)
            .allowIfSubType(Enum.class)
            .allowIfSubType("java.time.")
            .build();
    return new JsonKeysetCursorStrategy(Jackson2ObjectMapperBuilder.json()
            .polymorphicTypeValidator(validator)
            .build());
}

Sorting: binding a Sort argument

A Sort controller method parameter is populated from GraphQL arguments through a SortStrategy bean — AbstractSortStrategy implements the plumbing and leaves only the two extraction methods (which properties were requested, and in which direction) abstract:

@Bean
public SortStrategy sortStrategy() {
    return new AbstractSortStrategy() {
        @Override
        protected List<String> getProperties(Object source) {
            return List.of(((Map<?, ?>) source).get("field").toString());
        }

        @Override
        protected boolean isDescending(Object source) {
            return "DESC".equals(((Map<?, ?>) source).get("direction"));
        }
    };
}

No SortStrategy bean means no Sort argument binding — a controller method that declares one without a strategy registered simply never has it populated. Link: Spring for GraphQL — Sort.

  • Performance & N+1 — the language-agnostic batch-and- cache pattern @BatchMapping and BatchLoaderRegistry implement on the JVM.

  • Execution & Resolvers — the resolver signature and sync/async return shapes @BatchMapping/@SchemaMapping methods build on.

  • Federation — schema-level @key declarations and gateway composition behind the @EntityMapping methods this page covers on the resolver side.

  • Pagination — the Relay Connection/Edge/PageInfo shape that Window/Slice results are adapted into.

  • Spring Data JPA — repository basics, derived queries, and QuerydslPredicateExecutor/Specification support that this page’s auto-registered data fetchers build on.

  • Spring Boot: Controllers — the @QueryMapping/ @SchemaMapping annotations @BatchMapping and @EntityMapping sit alongside on the same controller class.