skip to content

You return Flux<User> from a @GetMapping. How is it serialized, and how do you make the endpoint actually stream results to the client instead of sending one JSON array?

level: seniorimportance: should knowfreq 50%

answer

  1. Default = one JSON array
  2. text/event-stream = SSE data: frames
  3. application/x-ndjson = one JSON per line
  4. produces attribute flips behavior
  5. Infinite Flux needs streaming media type

basics

~10 s

By default Flux<User> is serialized as a single JSON array (application/json). To stream element-by-element, set produces to text/event-stream (SSE) or application/x-ndjson, so each item is flushed as it is emitted.

solid answer

~40 s

With the default `application/json` content type, a `Flux<User>` is rendered as **one JSON array** — WebFlux still subscribes reactively and doesn't necessarily buffer everything, but the client sees a normal array and typically waits for the stream to complete before parsing. To get true item-by-item streaming you change the media type via `produces`: `MediaType.TEXT_EVENT_STREAM_VALUE` emits **Server-Sent Events** (each element as a `data:` event, ideal for browsers/`EventSource`), and `MediaType.APPLICATION_NDJSON_VALUE` (`application/x-ndjson`) emits **newline-delimited JSON**, one object per line, great for backend-to-backend streaming. With these types each `User` is encoded and flushed as it's emitted, so a client can consume results incrementally and the server holds far less in memory. An infinite `Flux` only makes sense with a streaming media type. Same annotation model — the media type flips the behavior.

code

java · 20 lines
java
@RestController
@RequestMapping("/users")
class UserStreamController {
    private final UserRepository repo;
    UserStreamController(UserRepository repo) { this.repo = repo; }

    // default: serialized as a single JSON array
    @GetMapping
    Flux<User> all() { return repo.findAll(); }

    // SSE: each element flushed as a data: event
    @GetMapping(value = "/sse",
                produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    Flux<User> sse() { return repo.findAll(); }

    // NDJSON: one JSON object per line, flushed as emitted
    @GetMapping(value = "/ndjson",
                produces = MediaType.APPLICATION_NDJSON_VALUE)
    Flux<User> ndjson() { return repo.findAll(); }
}

go deeper

for a junior

Know the default is a JSON array and streaming needs a different media type.

for a middle

Name text/event-stream and application/x-ndjson and their typical use cases.

for a senior

Explain memory/TTFB/backpressure benefits, 406 negotiation, and infinite-Flux constraints.

for a principal

Reason about proxy/gzip buffering defeating streaming and end-to-end backpressure requiring a reactive source.

## What `Flux<T>` means on the response side A `Flux<User>` is a stream of 0..N users. How Spring writes it to the HTTP response depends entirely on the **negotiated media type**, controlled by the `produces` attribute of `@GetMapping`/`@RequestMapping` (and the client's `Accept` header). ### Default: `application/json` → a JSON array ```java @GetMapping(value = "/users") // produces application/json by default Flux<User> all() { return repo.findAll(); } ``` The reactive Jackson encoder renders the whole `Flux` as **one JSON array**: `[ {...}, {...}, ... ]`. Internally WebFlux subscribes and writes incrementally, but semantically the client receives a single well-formed array and standard JSON clients wait for the closing `]` before they can parse it. This is the right default for normal collection endpoints. **Do not** return an infinite/never-completing `Flux` with this type — the array never closes. ### Streaming option A: Server-Sent Events — `text/event-stream` ```java @GetMapping(value = "/users/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) Flux<User> stream() { return repo.findAll().delayElements(Duration.ofMillis(500)); } ``` Each emitted `User` is wrapped as an SSE `data:` frame and **flushed immediately**. SSE is a long-lived HTTP response the browser consumes via the `EventSource` API. Use it for live feeds, progress, dashboards. You can also return `Flux<ServerSentEvent<User>>` to control event `id`, `event` name, `retry`, and comments. ### Streaming option B: NDJSON — `application/x-ndjson` ```java @GetMapping(value = "/users/stream", produces = MediaType.APPLICATION_NDJSON_VALUE) Flux<User> stream() { return repo.findAll(); } ``` Emits **newline-delimited JSON**: one complete JSON object per line, flushed as produced. It's the pragmatic choice for **service-to-service** streaming — no SSE framing overhead, easy to parse line-by-line, and `WebClient` consumes it naturally with `.retrieve().bodyToFlux(User.class)`. ## Why streaming media types matter - **Memory**: the server encodes and releases each element instead of holding the full result set. - **Latency / TTFB**: the client starts receiving data with the first element rather than after the last. - **Backpressure**: the reactive write path honors demand, so a slow client naturally slows the source (with a compatible source like R2DBC). - **Infinite streams**: only viable with a streaming media type. ## Gotchas - Content negotiation still applies: if the client's `Accept` doesn't match `produces`, you get **406 Not Acceptable**. - SSE requires the client to speak SSE (`EventSource` or an SSE-aware client); a plain JSON client won't parse `data:` frames. - Buffering in front (some proxies, gzip, or `Transfer-Encoding` handling) can defeat streaming — verify the flush actually reaches the client. - Returning a `Flux` doesn't magically make a blocking source non-blocking; the source must be reactive to gain real streaming/backpressure benefits. ## When to use which - Finite normal collection → default JSON array. - Browser live updates → `text/event-stream` (SSE). - Backend-to-backend or CLI streaming → `application/x-ndjson`.

  • What goes wrong if you return an infinite Flux with the default application/json media type?
    The response is a JSON array that never closes — the ']' is never written, so the connection stays open forever and standard JSON clients can never finish parsing. Infinite/long-lived streams must use text/event-stream or application/x-ndjson so each element is a self-contained frame/line.
  • How does a WebClient consume an NDJSON streaming endpoint?
    webClient.get().uri("/users/ndjson").retrieve().bodyToFlux(User.class) — it decodes each newline-delimited object into a Flux element as it arrives, preserving streaming and backpressure end-to-end.

saying these in an interview costs you the question

  • Thinking Flux<User> streams element-by-element by default over application/json
  • Returning an infinite Flux as a plain JSON array
  • Believing SSE and NDJSON are interchangeable for browser EventSource clients
  • Assuming streaming works even when the underlying source is blocking

context