The Dinner Rush: Building a Resilient Kitchen
Our restaurant is now a model of modern efficiency.
We can handle complex orders and manage live state (StateFlow) and events (SharedFlow). But let’s agree, we’ve been operating under ideal conditions.
What happens when the Saturday night dinner rush hits?
- The kitchen (
producer) starts churning out dishes far faster than the waiters (consumers) can deliver them. - A specialized task like butchering meat (
IO-intensive) is done right next to the delicate plating station (UI-bound), causing chaos :(. - One dish gets burnt, and the entire buffet line shuts down in response.
To survive the rush, our kitchen needs to be more than just efficient. It needs to be resilient.
Backpressure: When the Chef is Too Fast
Backpressure is what happens when a producer emits items faster than a consumer can process them.
By default, Flow is sequential. The chef waits for the waiter to deliver one dish before starting the next. This is safe but not always performant.
Let’s see example for fast chef and a slow waiter:
| |
Output:
| |
The chef is constantly blocked, waiting for the slow waiter. We can do better.
Strategy 1: buffer() - The Warming Table
The buffer() operator runs the producer coroutine concurrently with the consumer, with a buffer in between.
The chef can place dishes on a warming table without waiting for the waiter.

| |
Output:
| |
The chef finishes cooking almost instantly, and the total time is now dominated only by the slow waiter.
Strategy 2: conflate() - The “Latest Special” Board
What if we only care about the most recent value? conflate() is a strategy where a slow consumer skips intermediate values.
If the chef puts down three new dishes while the waiter is busy, the waiter will ignore the first two and just deliver the latest one.

| |
Output:
| |
Strategy 3: collectLatest() - The Search Bar
This collector processes only the latest value, but it goes a step further. If a new value arrives while the previous one is being processed, it cancels the old processing block and starts over with the new value. This is the perfect pattern for handling rapid-fire UI events like search queries.

| |
Output:
| |
flowOn(): Keeping the Kitchen Organized
By default, the producer and the collector run in the same coroutine and on the same thread.
This can be a problem if the chef is doing heavy work (like butchering on an IO thread) that shouldn’t happen at the delicate plating station (Main UI thread).
The flowOn() operator changes the execution context for the upstream code (the producer and any operators before it).

| |
Output:
| |
flowOn acts as a boundary. The heavy kitchen work stays off the main thread.
catch(): Handling a Burnt Dish
What happens if something goes wrong in the stream? By default, an exception will terminate the flow and crash the collector.
| |
The catch operator provides a declarative way to handle upstream exceptions.
| |
Output:
| |
The stream didn’t crash. The error was handled gracefully.
Note:
catchcan only handle exceptions from upstream operators. It can’t catch an exception in thecollectblock itself !
Wrap-up
The kitchen is now officially resilient and ready for the dinner rush.
- Backpressure Strategies: Manages fast producers and slow consumers using buffer (concurrency), conflate (latest value), and collectLatest (cancellable work).
- flowOn: Assignes specific parts of stream to the correct context, keeping code organized and UI responsive.
- catch: Use to handle errors declaratively within stream, preventing crashes and allowing for recovery.
What’s Next in Part 5?
We’ve designed an incredible restaurant, trained our staff, and built resilient systems. But how do we prove it all works without opening for business? How can we be sure the chef’s timing is right and the waiter’s logic is good?
In the final part, we will dive into the essential topic of Testing Coroutines and Flows.
We’ll explore the kotlinx-coroutines-test library, learn how to control virtual time, and write stable, reliable tests for our asynchronous world.