How do you write a Spring WebFlux controller handler using Kotlin coroutines, and how do suspend functions and Flow<T> return types map onto the reactive model?
answer
- suspend fun -> Mono, Flow -> Flux
- needs kotlinx-coroutines-reactor
- Unit -> Mono<Void>, null -> empty Mono
- runs on event loop — never block
- sequential-looking async, no .block()
basics
~20 sMark the handler method suspend fun and return a plain value or a Flow<T> instead of Mono/Flux. Spring adapts a suspend function to a single-value response and a Flow to a streaming (many-value) response automatically.
solid answer
~40 sIn an `@RestController`, WebFlux lets you write handlers as `suspend fun` so you use ordinary sequential Kotlin code instead of chaining `Mono`/`Flux` operators. A suspend handler that returns `T` is adapted to `Mono<T>` (single value, or empty for `Unit`); a handler returning `Flow<T>` is adapted to `Flux<T>` (a stream of many values). Under the hood the framework bridges the coroutine to Reactor, so nothing blocks the event loop as long as you never call blocking code directly. This requires `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` on the classpath. Inside the handler you can call other suspend functions (e.g. a suspending WebClient call or an R2DBC/coroutine repository) directly, and the handler simply suspends while awaiting them — no `.block()`, no callback nesting.
code
kotlin · 19 lines@RestController
@RequestMapping("/api/users")
class UserController(private val service: UserService) {
// suspend fun -> Mono<UserDto>
@GetMapping("/{id}")
suspend fun get(@PathVariable id: Long): UserDto =
service.load(id) // calls a suspending repo/WebClient, no .block()
// Flow<T> -> Flux<T>, streamed as Server-Sent Events
@GetMapping(produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun stream(): Flow<UserDto> = service.streamAll()
// suspend + Unit -> Mono<Void> (204/empty completion)
@DeleteMapping("/{id}")
suspend fun delete(@PathVariable id: Long) {
service.delete(id)
}
}go deeper
Know the two mappings: suspend fun -> single value (Mono), Flow -> stream (Flux); and that you write sequential code without .block().
Explain Unit/null/nullable mappings, the required kotlinx-coroutines-reactor dependency, and streaming request bodies as Flow.
Discuss the CoroutinesUtils/ReactiveAdapterRegistry bridge and the event-loop blocking hazard with the withContext(Dispatchers.IO) remedy.
Weigh coroutine handlers vs raw Reactor for team ergonomics, interop cost, and observability; reason about backpressure/cancellation preservation across the Flow->Flux boundary.
**The problem it solves.** Spring WebFlux is a non-blocking, reactive web stack. Its native return types are Project Reactor's `Mono<T>` (0..1 values) and `Flux<T>` (0..N values). Writing everything as chains of `map`/`flatMap`/`zip` operators is powerful but verbose and hard to read. Kotlin coroutines let you write asynchronous code that *looks* sequential (`val user = repo.findById(id)`), and WebFlux has first-class support for them since Spring Framework 5.2. **`suspend fun` handlers.** A `suspend` function is a Kotlin function the compiler can pause ("suspend") at await points and resume later without blocking a thread. In a controller you write: ```kotlin @GetMapping("/users/{id}") suspend fun getUser(@PathVariable id: Long): UserDto = service.load(id) ``` WebFlux detects the `suspend` modifier via reflection and wraps the call in `org.springframework.core.CoroutinesUtils.invokeSuspendingFunction`, which turns the invocation into a `Mono`. Mapping rules: - `suspend fun ...(): T` → **`Mono<T>`** (single value). - `suspend fun ...(): T?` returning `null` → an **empty `Mono`** (typically a 200 with no body, or 404 depending on config). - `suspend fun ...(): Unit` → **`Mono<Void>`** (completes with no body). - `fun ...(): Flow<T>` → **`Flux<T>`** (a stream; you do NOT need `suspend` on a `Flow`-returning function because `Flow` is itself the async carrier). **`Flow<T>` return types.** `Flow<T>` is the coroutines equivalent of a cold, back-pressured stream (the coroutines analogue of `Flux`). Returning it produces a streaming response: ```kotlin @GetMapping("/users", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) fun streamUsers(): Flow<UserDto> = service.streamAll() // Flow -> Flux ``` Spring converts the `Flow` to `Flux` via `kotlinx-coroutines-reactor`'s `asFlux()` (through the `ReactiveAdapterRegistry`), preserving backpressure and cancellation. **Required dependency.** The whole bridge needs `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` (which brings `kotlinx-coroutines-core`). Without it, `suspend`/`Flow` handlers are not recognized as reactive return types and you get startup/handler-mapping errors. **Request body.** You can accept `@RequestBody` as a deserialized object directly, or as a `Flow<T>` for streaming input: ```kotlin suspend fun create(@RequestBody dto: CreateReq): UserDto suspend fun bulk(@RequestBody items: Flow<Item>) { ... } ``` **Key gotcha — never block.** A suspend handler runs on the Reactor/Netty event-loop threads. Calling blocking APIs (JDBC, `Thread.sleep`, `.block()`, blocking file/HTTP I/O) directly stalls the event loop and destroys throughput. Offload blocking work with `withContext(Dispatchers.IO) { ... }`. **When to use.** Prefer coroutine handlers in a Kotlin WebFlux codebase for readability; use plain `Mono`/`Flux` when interop with existing reactive libraries is cleaner or when the team is not on coroutines. Coroutines and Reactor interoperate freely, so you can mix them.
- Why must you avoid calling JDBC or Thread.sleep directly inside a suspend handler?The handler executes on the non-blocking Reactor event-loop threads. Blocking them stalls all in-flight requests sharing that thread, collapsing throughput. Wrap blocking calls in `withContext(Dispatchers.IO)` so they run on a thread pool meant for blocking work.
- What happens if a suspend handler returns a nullable type and yields null?It is adapted to an empty `Mono`, i.e. a completion with no value. Depending on configuration/content that typically produces a 200 with empty body; you usually throw or return a `ResponseEntity` to signal 404 explicitly.
saying these in an interview costs you the question
- Claiming you must return Mono/Flux even with coroutines (defeats the point).
- Thinking Flow needs the suspend modifier on the function (it does not).
- Believing coroutines make blocking JDBC safe on the event loop.
- Assuming no extra dependency is needed (kotlinx-coroutines-reactor is required).