Flow โ Reactor: Bridging Coroutines and WebFlux
One conversion call turns a Reactor Flux into an idiomatic Kotlin Flow โ and back into a streaming WebFlux response.
Spring WebFlux is built on Project Reactor, whose core types are Mono (zero or one item) and Flux (zero to many items). Kotlin coroutines model the same ideas with suspend functions (one value) and Flow (a cold stream of many values). Because both worlds describe asynchronous, backpressure-aware streams, the kotlinx-coroutines-reactor and kotlinx-coroutines-reactive libraries give you lossless, two-way conversions so you can write idiomatic suspend/Flow code while still talking to the reactive Spring stack underneath.
The most common direction is consuming Reactor from coroutines. Calling Flux.asFlow() turns any Flux<T> into a Flow<T>, while a Mono<T> becomes a suspend-friendly value via .awaitSingle(), .awaitSingleOrNull(), or .awaitFirstOrNull(). These adapters preserve cancellation and backpressure: when the collecting coroutine is cancelled, the underlying Reactor subscription is disposed; and because Flow is cold, nothing runs until you actually collect. This lets a reactive repository (for example a Spring Data R2DBC ReactiveCrudRepository that returns Flux) be consumed inside a regular suspend handler as if it were a sequence.
The reverse direction matters when a framework expects a reactive type. Flow.asPublisher() produces a generic Reactive Streams Publisher<T>, and Flow.asFlux() (from kotlinx-coroutines-reactor) produces a Reactor Flux<T> specifically. A single suspend result can be wrapped with mono { ... } to build a Mono whose body is a coroutine. Use these when an API only accepts Publisher/Flux/Mono โ for instance wiring a Flow into a reactive Kafka sender, a WebClient body, or any operator that has no coroutine equivalent.
An important subtlety is context propagation. asFlux() and asPublisher() accept (or capture) a CoroutineContext, because Reactive Streams subscribers may request items from threads that have no coroutine context attached. When converting a Flow back to a publisher you usually let the operator capture the surrounding context, but be aware that the resulting publisher can be subscribed to from anywhere, so avoid relying on a thread-local that only exists at construction time. Conversely, asFlow() runs your collector in whatever coroutine context you collect it in, not on Reactor's scheduler.
Collecting a Flow inside a coroutine handler is the everyday pattern. In a suspend controller method you can call repository.findActive().asFlow() and then use familiar operators โ map, filter, take, onEach, buffer โ and terminal collectors like toList(), first(), or collect { }. This keeps transformation logic in plain Kotlin instead of the Reactor operator DSL, which is easier to read, test, and debug, while the suspension and cancellation semantics still flow through to the database driver.
Returning a Flow directly is where Spring's coroutine support shines. Since Spring Framework 5.2, a WebFlux @RestController handler may be a suspend function and may return a Flow<T> directly; Spring internally adapts it back to a Flux for the HTTP response. If you set produces = MediaType.TEXT_EVENT_STREAM_VALUE (or APPLICATION_NDJSON_VALUE), each element is flushed to the client as it is emitted, giving you true server-side streaming without ever writing a single Reactor type in your controller.
Putting it together, a clean WebFlux service can be coroutine-first end to end: the repository returns Flux, you adapt with asFlow(), transform with coroutine operators, and return the Flow straight from a suspend handler. Spring closes the loop by converting it back to a Flux for the wire. You only reach for asFlux()/asPublisher()/mono { } at the seams where a non-coroutine reactive API forces the issue โ and even those conversions are one-liners.
A few practical guardrails: prefer awaitSingleOrNull() over awaitSingle() when a Mono may be empty (the latter throws NoSuchElementException), remember that Flow is cold so re-collecting re-runs the upstream, and add buffer() or conflate() if a fast producer outpaces a slow collector. Keep the conversion boundary thin โ convert once at the edge, do your business logic in Flow, and let Spring handle the final adaptation. These libraries are read-and-copy dependencies: add kotlinx-coroutines-reactor to your Gradle build, they are not runnable in a browser sandbox.
import kotlinx.coroutines.flow.*import kotlinx.coroutines.reactive.awaitSingleOrNullimport reactor.core.publisher.Fluximport reactor.core.publisher.Monointerface UserRepo {fun findActive(): Flux<User> // Spring Data R2DBC stylefun findById(id: Long): Mono<User>}class UserService(private val repo: UserRepo) {// Flux -> Flow, then plain Kotlin operatorsfun activeNames(): Flow<String> =repo.findActive().asFlow().filter { it.enabled }.map { it.name }// Mono -> suspend value (null-safe; awaitSingle() would throw if empty)suspend fun nameOf(id: Long): String? =repo.findById(id).awaitSingleOrNull()?.name}data class User(val id: Long, val name: String, val enabled: Boolean)
Consuming Reactor types from a suspend handler: Flux.asFlow() + Mono awaitSingleOrNull(). The reactive repository keeps backpressure end-to-end.
import kotlinx.coroutines.flow.*import org.springframework.http.MediaTypeimport org.springframework.web.bind.annotation.*@RestController@RequestMapping("/api/users")class UserController(private val service: UserService) {// Server-Sent Events: streamed item by item@GetMapping("/stream", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])fun stream(): Flow<String> = service.activeNames()// suspend handler returning a single value@GetMapping("/{id}")suspend fun one(@PathVariable id: Long): String =service.nameOf(id) ?: "unknown"}
Returning a Flow as a streaming WebFlux response. With suspend + Flow + TEXT_EVENT_STREAM_VALUE, Spring flushes each element as it is emitted โ no Reactor types in the controller.
import kotlinx.coroutines.flow.*import kotlinx.coroutines.reactor.asFluximport kotlinx.coroutines.reactor.monoimport kotlinx.coroutines.reactive.asPublisherimport kotlinx.coroutines.delayimport org.reactivestreams.Publisherimport reactor.core.publisher.Fluximport reactor.core.publisher.Monoval source: Flow<Int> = flowOf(1, 2, 3)// Flow -> Reactor Flux (Reactor-specific)val asFlux: Flux<Int> = source.asFlux()// Flow -> generic Reactive Streams Publisherval asPub: Publisher<Int> = source.asPublisher()// single suspend result -> Mono via a coroutine builderfun loadToken(): Mono<String> = mono {delay(50)"token-" + (1..3).sum()}
The reverse bridge: Flow -> Reactor for APIs that demand a reactive type. asFlux()/asPublisher() come from kotlinx-coroutines-reactor; mono { } wraps a single suspend result.
import kotlinx.coroutines.flow.*import kotlinx.coroutines.runBlockingfun numbers(): Flow<Int> = flow {for (i in 1..5) emit(i) // cold: re-runs on every collect}fun main() = runBlocking {val evens = numbers().filter { it % 2 == 0 }.map { it * 10 }.toList()println(evens) // [20, 40]// collecting again re-runs the upstreamnumbers().take(2).collect { println("got " + it) }}
Pure kotlinx.coroutines demo of cold-Flow semantics and operators you reuse after asFlow(). No Spring/Reactor needed โ runs on stdlib + coroutines.
Arena IDE๐ง Check your understanding
0/1 ยท 0/1 answered1. In a WebFlux @RestController, what is the idiomatic way to expose a coroutine Flow<T> as an item-by-item streaming HTTP response?