skip to content

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?

level: principalimportance: should knowfreq 30%

answer

  1. return: value / Mono / Flux / CompletableFuture / suspend / Flow
  2. query Flux -> collected list; subscription Flux -> streamed
  3. Mono = one deferred value; CF ~ Mono
  4. ReactiveAdapterRegistry adapts any Publisher
  5. off-request-thread: no ThreadLocal; use GraphQLContext

basics

~20 s

Handlers 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 s

Annotated 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
java
@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

for a junior

Know handlers can return Mono/Flux/CompletableFuture, not just plain values.

for a middle

Explain Mono = one value, and that a query Flux is collected while a subscription Flux streams.

for a senior

Discuss ReactiveAdapterRegistry, DataLoader with CompletableFuture, and off-request-thread context propagation.

for a principal

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

context