What async/reactive return types can annotated GraphQL controller methods use, and how does Spring treat Mono vs Flux across query, subscription, and field mappings?
answer
- return: value / Mono / Flux / CompletableFuture / suspend / Flow
- query Flux -> collected list; subscription Flux -> streamed
- Mono = one deferred value; CF ~ Mono
- ReactiveAdapterRegistry adapts any Publisher
- off-request-thread: no ThreadLocal; use GraphQLContext
basics
~20 sHandlers may return a plain value, a Mono, a Flux, or a CompletableFuture. Mono/CompletableFuture resolve one value asynchronously. For a query, a Flux is collected into a list; for a subscription, a Flux is streamed. Kotlin suspend functions and Flow are also supported.
solid answer
~40 sAnnotated GraphQL controller methods can return the value directly (blocking), or an async wrapper: `Mono<T>` / `CompletableFuture<T>` for a single deferred value, or `Flux<T>` for many. The crucial rule is operation-dependent: on `@QueryMapping`/`@MutationMapping`/`@SchemaMapping`, a returned `Flux` is **collected** into a `List` and delivered as one result — it does not stream. On `@SubscriptionMapping`, the `Flux` (Publisher) is **streamed** element-by-element to the client. `Mono` always resolves to a single value. Spring adapts these via its `ReactiveAdapterRegistry`, so any Reactive Streams Publisher works, and Kotlin `suspend`/`Flow` too. Because reactive execution runs off the request thread, avoid ThreadLocal assumptions and propagate context through `GraphQLContext`/`@ContextValue`. Async returns also let you compose `DataLoader` results (`Mono`/`CompletableFuture`) for non-blocking batched field resolution.
code
java · 27 lines@Controller
public class LibraryController {
// Mono -> single deferred value
@QueryMapping
public Mono<Book> bookById(@Argument String id) {
return bookRepo.findById(id);
}
// Flux under a QUERY -> COLLECTED into a List (one result, not streamed)
@QueryMapping
public Flux<Book> books() {
return bookRepo.findAll();
}
// Async field resolver via DataLoader (non-blocking, batched)
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<String, Author> loader) {
return loader.load(book.authorId());
}
// Flux under a SUBSCRIPTION -> STREAMED element-by-element
@SubscriptionMapping
public Flux<Book> bookAdded() {
return bookEvents.asFlux();
}
}go deeper
Know handlers can return Mono/Flux/CompletableFuture, not just plain values.
Explain Mono = one value, and that a query Flux is collected while a subscription Flux streams.
Discuss ReactiveAdapterRegistry, DataLoader with CompletableFuture, and off-request-thread context propagation.
Reason about blocking-vs-reactive stack choices, thread-starvation risks, security-context propagation, and batching/backpressure architecture.
Spring for GraphQL integrates with both blocking and reactive stacks. A handler's return type tells Spring how to obtain the field value. **Supported return types** - **Plain value** (`Book`, `List<Book>`): resolved synchronously on the calling thread. - **`Mono<T>`**: a Reactive Streams single-value publisher; Spring subscribes and uses the emitted value (or empty → null). - **`Flux<T>`**: a multi-value publisher — behavior depends on the operation (below). - **`CompletableFuture<T>`**: async single value, equivalent to `Mono` for GraphQL's purposes (graphql-java is `CompletableFuture`-native). - **Any Reactive Streams `Publisher`**: adapted via Spring's `ReactiveAdapterRegistry` (RxJava, etc.). - **Kotlin `suspend` functions** and **`Flow<T>`**: supported and mapped to Mono/Flux semantics. **The pivotal Flux rule — operation matters** - On a **query / mutation / field mapping** (`@QueryMapping`, `@MutationMapping`, `@SchemaMapping`): a `Flux<T>` is **collected** (like `.collectList()`) into a single `List<T>` result. It is NOT streamed to the client — GraphQL queries produce one response. - On a **subscription** (`@SubscriptionMapping`): the returned `Publisher`/`Flux` is **streamed**; each emitted element becomes a separate pushed result over WebSocket/SSE. So the same `Flux<Comment>` means "give me the list" under a query and "push each as it arrives" under a subscription. This is the single most tested nuance. **Mono** always maps to a single deferred value regardless of operation. **Threading & context** - Reactive returns are subscribed on reactive scheduler threads, not the servlet request thread. Do not depend on `ThreadLocal`/request scope inside the pipeline. - Propagate needed state via `GraphQLContext` (write it, read with `@ContextValue`) or Reactor `Context`. Security context propagation must be explicit. - Blocking work inside a reactive pipeline should be offloaded (`subscribeOn(Schedulers.boundedElastic())`) to avoid stalling event-loop threads (critical on WebFlux). **DataLoader & async** Batched field resolution pairs naturally with async returns: a `@SchemaMapping` can return `CompletableFuture<Author>` from `DataLoader.load(key)`, letting graphql-java batch keys and resolve them without blocking. `@BatchMapping` can return `Mono<Map<K,V>>` or `Flux<V>`. **Error handling** - An error terminating a `Mono`/`Flux` becomes a GraphQL error for that field (mapped via `DataFetcherExceptionResolver`). For subscriptions, an `onError` ends the stream. **Gotchas / senior-level pitfalls** - Expecting a query's `Flux` to stream — it won't; it's collected. - Blocking (`.block()`) inside a WebFlux handler thread → thread starvation/deadlock. - Assuming `ThreadLocal`-based security/tenant context survives onto reactive threads. - Returning `Flux` where the schema type is a single object (non-list) — mismatched cardinality. - Mixing blocking JDBC in a reactive pipeline without offloading. **When to use async**: on the WebFlux stack, or to compose non-blocking I/O and DataLoader batching. On the servlet stack, returning `CompletableFuture`/`Mono` still enables async, non-blocking field resolution and better throughput for I/O-bound resolvers.
- You return Flux<Book> from a @QueryMapping whose schema type is [Book]. Does the client receive a stream?No. Under a query the Flux is collected into a List and returned as a single response. Streaming only happens for @SubscriptionMapping.
- How do you carry per-request data (e.g. tenant, auth) into a reactive resolver that runs off the request thread?Write it into the GraphQLContext (or Reactor Context) up front and read it with @ContextValue / from the context inside the pipeline, rather than relying on ThreadLocal or request scope.
saying these in an interview costs you the question
- Claiming a Flux from a query streams to the client (it's collected into a list)
- Believing CompletableFuture isn't supported (it is, and is graphql-java native)
- Assuming ThreadLocal security/tenant context propagates to reactive threads automatically
- Calling .block() inside a WebFlux resolver