Overview
GraphQL replaces many resource endpoints with one typed query language: clients declare the shape of the data they need and the server resolves exactly that. On the JVM nearly every option is a layer over the same engine, graphql-java, which parses, validates and executes queries but deliberately ships no HTTP transport — the frameworks supply the transport, the resolver wiring and the developer ergonomics. The recurring engineering problems are the same in every stack: schema ownership (schema-first vs code-first), the N+1 resolver trap, and picking a transport for subscriptions.
Key points
- graphql-java is the engine:
GraphQLSchema,DataFetcher,ExecutionInput; everything else in the JVM GraphQL world builds on it. - Spring for GraphQL is the official Spring integration: schema-first (
.graphqlsfiles), resolvers as annotated controllers (@QueryMapping,@MutationMapping,@SchemaMapping), transports over MVC, WebFlux and WebSocket. - Quarkus SmallRye GraphQL implements MicroProfile GraphQL: code-first —
annotate a
@GraphQLApiclass with@Query/@Mutationand the schema is derived from the Java types. - Micronaut GraphQL is a thin integration: it wires graphql-java into the Micronaut
HTTP server and DI; you build the
GraphQLbean largely by hand. - Netflix DGS brought annotations (
@DgsComponent,@DgsQuery) and codegen to Spring Boot; since its unification with Spring for GraphQL the two share a runtime, so the choice is mostly annotation taste and federation tooling. - Schema-first vs code-first: schema-first treats the SDL as the reviewed contract (Spring, DGS); code-first derives it from types (SmallRye) — faster to start, easier to drift.
- N+1 is the defining performance trap: naive field resolvers fire one query per parent; DataLoader (java-dataloader) batches and caches per-request.
- Subscriptions need a streaming transport — WebSocket (
graphql-wsprotocol) or SSE — and return a reactive stream from the resolver.
Details
Stack comparison
| Concern | Spring for GraphQL | Quarkus SmallRye GraphQL | Micronaut GraphQL | Netflix DGS |
|---|---|---|---|---|
| Spec basis | graphql-java | MicroProfile GraphQL over graphql-java | graphql-java | graphql-java (shared Spring runtime) |
| Schema style | schema-first | code-first | either (manual wiring) | schema-first + codegen |
| Resolver style | @Controller + @SchemaMapping |
@GraphQLApi + @Query |
DataFetcher beans |
@DgsComponent + @DgsQuery |
| Batching | @BatchMapping / DataLoader registry |
@Source batch methods |
manual DataLoader | DGS data loaders |
| Subscriptions | WebSocket (graphql-ws), Flux return |
SSE/WebSocket, Multi return |
WebSocket support | WebSocket via Spring runtime |
The N+1 problem and DataLoader
A query for 100 orders each resolving customer executes the customer fetcher 100 times.
DataLoader collects the keys requested during one execution step, calls a batch function
once (ids -> Map<Long, Customer>), and caches within the request. Spring's
@BatchMapping and SmallRye's @Source batch form (a resolver taking List<Order>)
are declarative sugar over the same mechanism. Treat any resolver that touches a
database or remote service as a batching candidate by default.
Subscriptions transport
The de-facto WebSocket sub-protocol is graphql-ws (successor to the legacy
subscriptions-transport-ws); SSE is the simpler alternative when the flow is one-way.
Resolvers return Publisher/Flux/Multi; the framework bridges it onto the socket.
Subscriptions inherit every WebSocket scaling concern — see the WebSockets note — so
prefer them only when clients genuinely need push, not as a default.
Operational notes
- Disable or gate introspection and set query depth/complexity limits in production — an unbounded query language is a DoS surface.
- Errors travel in the
errorsarray alongside partialdata; map exceptions to typed error extensions rather than leaking messages. - Persisted queries / trusted documents shrink the attack surface and the payloads.
Examples
// Spring for GraphQL — schema-first resolver with batching
@Controller
class OrderController {
@QueryMapping
List<Order> orders() { ... }
@BatchMapping // one call for all orders in the request
Map<Order, Customer> customer(List<Order> orders) { ... }
}
// Quarkus SmallRye GraphQL — code-first
@GraphQLApi
public class OrderApi {
@Query
public List<Order> orders() { ... }
}
Related
- Backend protocols in Java — the map — parent overview of the protocol landscape.
- REST & HTTP APIs in Java — the default it competes with; GraphQL earns its keep when clients aggregate.
- WebSockets & SSE in Java — the transports subscriptions ride on.
- API design — contract and evolution principles that apply to SDL as much as to REST.