Spring for GraphQL is the Spring portfolio's GraphQL server, built on the GraphQL Java engine and auto-configured by Spring Boot. You describe the API in a schema, bind its fields to annotated controller methods, and the framework executes each incoming query against that wiring. Interviewers bring it up when a design has one client needing flexible reads across many entities or services, and they probe whether you understand what GraphQL moves around: the client decides the shape of the response, so the server has to cope with query shapes it never planned for.
The hub follows that path. Schema-first development covers the contract and how it is wired to code; annotated controllers are where handlers live. Batch loading and DataLoader answers the performance question GraphQL invites by design. Transports and interception explain how requests arrive and how headers and security reach a resolver. Error handling and cursor-based pagination are where GraphQL conventions differ most from REST, and federation is the answer when many teams sit behind one API.
Junior rounds stay on the mapping from schema to method. Senior and principal rounds turn to cost and ownership: batching, partial failures, paging under concurrent writes, and who owns which type once the schema is split across services. Learn the schema and controllers first; everything else in the hub is a refinement of that binding.
The schema is the contract
The SDL files are the API. Clients, reviewers and tooling read them; Java or Kotlin code only supplies values for what they declare. Because nothing ties the two together at compile time, a mismatch shows up at startup or at request time rather than in the compiler, which is why interviewers ask how you keep schema and handlers honest.
Every field has a resolver
Execution walks the query tree and asks a data fetcher for each selected field. Root fields under Query, Mutation and Subscription map to controller methods; fields on other types either come straight from the parent object's properties or from a method that receives that parent. Thinking per field, not per endpoint, is the shift from REST that most other answers depend on.
The client picks the shape, the server pays for it
A client can nest lists inside lists, so a naive per-field lookup multiplies into one call per parent. Batching those lookups per request belongs in the design of any field that crosses a table or a service boundary, not in a later tuning pass. Depth, cost and page-size limits belong to the same conversation.
Failure is per field
One request can succeed in part. A failing field becomes null plus an entry in the errors list, while its siblings still return, and the transport status usually stays 200. Error design therefore means choosing classifications and messages clients can act on, and deciding what a client sees when something unexpected breaks.
One execution, several transports
Queries and mutations normally travel over HTTP; subscriptions need a connection that stays open. Interceptors sit in front of execution on every transport, which makes them the place to move headers, identities and correlation ids into the context a resolver can read.
A graph can have many owners
Federation splits one schema across services, each owning some types and extending others by key. It trades a single codebase for composition rules, a router and cross-service query plans — worth it when team boundaries, not load, are the problem.
Follow one request. It arrives on the HTTP endpoint (or, for a subscription, on a WebSocket or RSocket connection) and passes through the interceptor chain, which can lift headers or the authenticated user into the request context. The execution engine parses and validates the query against the schema that GraphQlSource built at startup, then resolves fields top-down: root fields call controller methods, nested fields call schema mappings or read properties of the parent. Whenever a resolver asks for data through a DataLoader, its key waits in a queue until the engine dispatches the whole level as one batch. Exceptions from any resolver pass through the exception-resolution chain and become entries in errors next to whatever data did resolve.
The rest of the hub attaches to that path:
- Pagination wraps selected list fields so a controller can return a scrolled window of results and the framework renders it as a connection with cursors.
- Security usually authenticates at the endpoint with the ordinary filter chain and authorizes per method, relying on the context reaching resolvers that run asynchronously.
- Federation adds entity resolution to a subgraph, so a router can ask this service for objects by key in the middle of a plan that spans others.
The core binding, with the batching that keeps it from turning into N+1:
kotlin@Controller
class BookController(private val books: BookRepository, private val authors: AuthorClient) {
@QueryMapping
fun booksByGenre(@Argument genre: String): List<Book> = books.findByGenre(genre)
@BatchMapping
fun author(books: List<Book>): Map<Book, Author> {
val byId = authors.findByIds(books.map { it.authorId }.toSet())
return books.associateWith { byId.getValue(it.authorId) }
}
}
The two methods meet only through the schema: nothing in the code says that Book has an author field, so renaming either side without the other breaks the binding, not the build.
This guide assumes Spring for GraphQL on Spring Boot 3, from 1.3 onward. The project reached 1.0 in 2022 alongside Spring Boot 2.7, and several features interviewers ask about arrived later:
- 1.2 added pagination support — connection types, scrolling subranges and adapters over Spring Data's scroll results — along with
@GraphQlExceptionHandler methods and the schema inspection report that flags schema fields with no handler.
- 1.3 added federation support, built on the federation library for GraphQL Java, with entity mapping methods on controllers.
If an interviewer's codebase is older, say which features it would lack rather than assuming them. The annotation model for queries, mutations, schema mappings and batch mappings has been stable since 1.0, so answers about controllers and batching carry across every release line.
Spring for GraphQL sits on GraphQL Java, the engine that parses, validates and executes queries; Spring contributes the Boot auto-configuration, the annotated controller model, transports, interception, security integration and Spring Data support. Netflix DGS is the other widely used GraphQL framework for Spring applications, with its own annotation model and code generation around the same engine; a candidate should know both exist and that the concepts transfer.
GraphQL itself is usually weighed against REST and gRPC. REST keeps HTTP caching, status codes and simple per-resource endpoints; gRPC suits typed service-to-service calls; GraphQL fits clients that assemble varied views from many entities in one round trip, at the price of harder caching and per-field cost control. In federated designs, Spring services act as subgraphs behind a router such as Apollo's, which owns composition and query planning. See Federation Overview for the subgraph side.
GraphQL resolves each field independently, so a query returning N books where each book resolves its author fires 1 query for the books plus N separate author queries — the classic N+1 problem. Because it is data-source agnostic, GraphQL cannot auto-join like SQL. Batch loading fixes this: instead of resolving each author immediately, the resolver registers the author key and returns a deferred value. Once all N books have registered their keys, the framework dispatches a single batched call (e.g. findAllById(authorIds)) and hands each field its result. In Spring for GraphQL you get this with @BatchMapping controller methods or a DataLoader registered via BatchLoaderRegistry. It collapses N+1 into 2 round trips regardless of N.
The problem
A GraphQL query like:
graphql{ books { title author { name } } }
resolves fields top-down and independently. Spring for GraphQL first runs the books resolver (1 query → N books). Then, for each book, it runs the author field resolver — and if that resolver naively does authorRepository.findById(book.authorId()), you fire N more queries. Total: 1 + N = the N+1 problem. With 100 books that is 101 database round trips.
GraphQL is data-source agnostic — the engine has no idea your author field maps to a foreign key, so unlike a hand-written SQL JOIN it cannot fold the lookups together automatically. You must do it.
How batch loading fixes it
The core trick is deferred (lazy) resolution. Instead of loading the author immediately, the field resolver:
- Records the key it needs (the
authorId) and returns a promise/future that is not yet completed.
- The GraphQL engine keeps resolving sibling fields, so all N books register their author keys.
- When the engine finishes that level and is about to dispatch, it calls your batch function once with the full list of keys:
[id1, id2, … idN].
- Your batch function runs a single query —
findAllById(authorIds) — and returns the results; the engine completes each book's future with the matching author.
Result: 2 round trips (books + authors) instead of 1 + N.
In Spring for GraphQL
Two built-in mechanisms:
@BatchMapping — a controller method that receives a List<Book> (all the parents at that level) and returns a Map<Book, Author> (or an ordered List<Author>). Spring wires it to a DataLoader for you.
DataLoader + BatchLoaderRegistry — register a batch function manually; the resolver calls dataLoader.load(key) and returns the resulting CompletableFuture.
Why not just eager-fetch everything?
Because the client chooses the shape at runtime. You do not know in advance whether the client will even ask for author, so you cannot bake a JOIN into every query. Batch loading is demand-driven: it only batches the fields the client actually requested.
Gotchas
- Batching only helps when the same field is resolved for many parents at the same level — a single-object query sees no benefit.
- The batch result must be correlated back to each key (by map key or by list position); a mismatch silently returns wrong/null data.