Skip to Content
Course content

238: Reactive Streams with Kotlin Flow and WebFlux

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you something that used to be a huge headache before Kotlin Flow came along. Imagine we're building a logistics dashboard. We need a way to stream the GPS coordinates of a delivery truck to a frontend in real-time. If we just return a list of coordinates, the user has to refresh the page or we have to poll the server every few seconds. That's inefficient and feels clunky.

The problem with the "Wait and See" approach

My first instinct—or at least, the old way—was to just return a List. Let's look at what happens when we try that in a WebFlux controller:

@GetMapping("/truck/{id}/locations")
fun getTruckLocations(@PathVariable id: String): List<Location> {
    return locationService.getHistory(id) 
}

The problem here is that the server has to gather every single single coordinate into a list in memory before it can send a single byte to the client. If the truck has been driving for three days, that's a massive payload. The user sees nothing, then suddenly a huge wall of data hits their browser. That's not "reactive"; that's just a delayed bulk download.

Trying Flux, but feeling the Java friction

Since we're using WebFlux, the "native" way to handle this is Project Reactor's Flux. I tried this a while back, and while it works, it always felt like I was writing Java in a Kotlin file. It's a bit verbose and doesn't play as nicely with Kotlin's structured concurrency.

@GetMapping(value = ["/truck/{id}/locations"], produces = MediaType.TEXT_EVENT_STREAM_VALUE)
fun streamLocations(@PathVariable id: String): Flux<Location> {
    return locationService.getLocationStream(id)
}

This is better. By setting the produce type to TEXT_EVENT_STREAM_VALUE (Server-Sent Events), the browser keeps the connection open. But the Flux API is heavy. I wanted something that felt like a standard Kotlin collection but worked asynchronously. That's where Flow comes in.

Bridging Flow into the Web Layer

Now, let's rewrite the service layer using Flow. I love the flow { ... } builder because it lets me use delay() without blocking the entire thread. It's essentially a coroutine that can emit multiple values.

I'll set up a mock stream that simulates a truck moving toward a destination:

@Service
class LocationService {
    fun getLocationStream(truckId: String) = flow {
        val startPos = Location(40.7128, -74.0060) // NYC
        var currentPos = startPos
        
        while (true) {
            emit(currentPos)
            delay(1000) // Simulate a GPS ping every second
            currentPos = currentPos.copy(
                lat = currentPos.lat + 0.001, 
                lon = currentPos.lon + 0.001
            )
        }
    }
}

Now, here is the magic part. You might think we need to manually convert this Flow back into a Flux for WebFlux to understand it. I tried that at first using asFlux(), but I realized that's an unnecessary step. Spring WebFlux has built-in support for Kotlin Flow.

Let's update the controller:

@GetMapping(value = ["/truck/{id}/locations"], produces = MediaType.TEXT_EVENT_STREAM_VALUE)
fun streamLocations(@PathVariable id: String): Flow<Location> {
    return locationService.getLocationStream(id)
}

When I run this and hit the endpoint in my browser, I don't see a JSON array. Instead, I see a new line appearing every second: data: {"lat": 40.7128, "lon": -74.0060}. The server isn't holding onto a list; it's pushing data the moment it's emitted from the flow.

Handling the "Cold" nature of Flow

One thing I noticed during testing: if I connect two different browser tabs to this endpoint, they both start from the startPos. This is because flow { ... } is "cold"—it starts fresh for every collector.

If I wanted all users to see the same real-time truck position, I'd need a SharedFlow. But for a history stream or a per-user tracking session, this cold flow is exactly what we want. It's lightweight, it respects coroutine cancellation (if the user closes the tab, the while(true) loop stops immediately), and it keeps our controller code incredibly clean.




📋 Practical Task

Exercise: Building a Live System Resource Monitor

Your task is to create a WebFlux endpoint that streams the current memory usage of the JVM to a client in real-time using Kotlin Flow.

  • Create a ResourceService with a function getMemoryUsageStream() that returns a Flow<Long>.
  • Inside the flow, use a while(true) loop to emit the current free memory (use Runtime.getRuntime().freeMemory()) every 500 milliseconds.
  • Create a ResourceController with a GET mapping at /system/memory.
  • Ensure the endpoint uses MediaType.TEXT_EVENT_STREAM_VALUE so the data streams to the browser rather than buffering.
  • Verify that closing the browser tab stops the flow emissions (you can add a println inside the flow to observe this in the console).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.