rgoussu@goussu: ~/library/java/protocols
~/library/java/protocols cat graphql.md

GraphQL on the JVM

# graphql-java as the shared engine beneath Spring for GraphQL, SmallRye GraphQL, Micronaut GraphQL and Netflix DGS, plus N+1 and subscriptions.

Conceptsaved 2026-08-09 #java#protocols#graphql#frameworks

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 (.graphqls files), resolvers as annotated controllers (@QueryMapping, @MutationMapping, @SchemaMapping), transports over MVC, WebFlux and WebSocket.
  • Quarkus SmallRye GraphQL implements MicroProfile GraphQL: code-first — annotate a @GraphQLApi class with @Query/@Mutation and 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 GraphQL bean 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-ws protocol) 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 errors array alongside partial data; 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