Kotlin Coroutines: from the machine to production
Kotlin coroutines as their own subject — no Spring, no Reactor, no framework in the way. 118 lessons across 22 parts, from what suspend actually compiles to all the way up to running coroutines in production. Every practice runs against the REAL kotlinx.coroutines library — and against kotlinx-coroutines-test, so runTest's virtual clock makes the timing exact: "50,000 coroutines finished in 10 ms on one thread" is an assertion, not a promise.
Let’s figure it out.
Ask me about this course or where to start.
What `suspend` actually compiles to, why a suspended coroutine holds no thread, and what the three builders each give you.
- Step 1
What `suspend` really is
Every asynchronous story starts with the same problem: a thread that waits is a thread you cannot use. A JVM thread cost…
Start lesson - Step 2
Continuations: what the compiler actually generates
Coroutines feel like magic until you see the transform, and then they stop being magic forever. The compiler rewrites ev…
Start lesson - Step 3
The cost model: why 50,000 coroutines is unremarkable
Concurrency arguments usually stall on vibes, so put numbers on it. A JVM thread reserves roughly 1 MB of stack by defau…
Start lesson - Step 4
launch, async, runBlocking — and withContext
You cannot call a suspend function from ordinary code — lesson 1's rule — so something has to create the first coroutine…
Start lesson - Step 5
Capstone: sequential by default, concurrent on purpose
Part I ends where most real slowdowns begin. A handler calls three services, each takes 100 ms, and the endpoint takes 3…
Start lesson
Scopes, the Job tree and its lifecycle states, cancellation as a cooperative protocol, start modes, and the cleanup that silently never runs.
- Step 6
Scopes and the Job tree
A `CoroutineScope` sounds grand and is almost nothing: an interface with a single property, `coroutineContext`. What giv…
Start lesson - Step 7
The Job lifecycle
Every builder hands you a `Job`. `launch` returns one directly, `async` returns a `Deferred<T>` which *is* a `Job` with …
Start lesson - Step 8
coroutineScope vs supervisorScope
You have two scope functions and they look interchangeable. They are not. `coroutineScope { }` and `supervisorScope { }`…
Start lesson - Step 9
Cancellation is cooperative
Cancellation is what makes structured concurrency worth having: a disconnected client, a fired `withTimeout`, a destroye…
Start lesson - Step 10
Cleanup that survives cancellation
This is the cancellation bug that reaches production, and it is not a typo — it is the contract from lesson 8 applied co…
Start lesson - Step 11
Start modes: DEFAULT, LAZY, ATOMIC, UNDISPATCHED
`launch` and `async` both take a `start:` parameter, and almost nobody passes it. That is usually right — `CoroutineStar…
Start lesson - Step 12
Capstone: a job you can actually stop
Part II has given you four separate facts: a scope owns a Job tree, the failure policy is a choice, cancellation is coop…
Start lesson
Running things at the same time on purpose: async/await, deadlines, racing with select, capping the fan-out, and where a failure actually surfaces.
- Step 13
async and Deferred, in depth
`async` returns a `Deferred<T>`, which is a `Job` with a result attached. Everything you know about Jobs applies: it is …
Start lesson - Step 14
Timeouts and deadlines
A call with no deadline is a call that can hang forever, and in a system with a connection pool that is how one slow dep…
Start lesson - Step 15
select — waiting on several things at once
Everything so far waits for one *named* thing: `await()` this Deferred, `receive()` from this channel, `join()` this Job…
Start lesson - Step 16
Bounded fan-out
Part I sold you on cheap concurrency, and it is true: launching coroutines costs almost nothing. But the coroutines are …
Start lesson - Step 17
Where an exception actually goes
Every confusing thing about coroutine exceptions comes from one fact: **`try/catch` unwinds the call stack, and a corout…
Start lesson - Step 18
Capstone: a call that is bounded, timed and retried
Part III has given you the pieces separately: `async` for concurrency, `withTimeoutOrNull` for a deadline, `select` for …
Start lesson
The CoroutineContext as a typed map, which dispatcher to pick and why, limitedParallelism, and how a thread pool actually starves.
- Step 19
CoroutineContext: the typed set every coroutine carries
A `CoroutineContext` is an **immutable, indexed set of elements**, where the index is the element's type. It behaves lik…
Start lesson - Step 20
Dispatchers in production
A dispatcher decides which thread runs a coroutine when it resumes. There are four you will meet, and picking between th…
Start lesson - Step 21
limitedParallelism and custom dispatchers
`Dispatchers.Default` and `Dispatchers.IO` are not two thread pools. They are two **views of the same pool**, with diffe…
Start lesson - Step 22
CPU-bound, blocking, and the starvation in between
Every dispatcher choice reduces to one question. **Does this code occupy a thread while making no progress?** If it does…
Start lesson - Step 23
CoroutineContext, names and correlation ids
A `CoroutineContext` is an immutable set of elements indexed by type: a `Job`, a dispatcher (`ContinuationInterceptor`),…
Start lesson
Shared mutable state and the four ways to survive it: confinement, atomics, Mutex and Semaphore — plus the deadlocks each one invites.
- Step 24
Shared mutable state
The most dangerous misconception about coroutines is that a single-threaded dispatcher makes shared state safe. It does …
Start lesson - Step 25
The four races, and how to make one reproduce
The previous lesson showed *that* shared mutable state breaks. This one names the four shapes it breaks in, because each…
Start lesson - Step 26
Mutex: a lock that suspends instead of blocking
`kotlinx.coroutines.sync.Mutex` looks like `synchronized` and is a different animal. When it is contended it **suspends*…
Start lesson - Step 27
Semaphore: a quota that travels with the coroutine
A `Semaphore(permits)` is a counter with a queue. `acquire()` takes a permit or suspends until one is free; `release()` …
Start lesson - Step 28
Deadlock, livelock, starvation
All three of these look identical from the outside — the work stops — and they need completely different fixes. One ques…
Start lesson
The queue between coroutines: capacity, overflow policy, worker pools, pipelines, actors, and state owned by exactly one coroutine.
- Step 29
Channels: the queue between coroutines
A `Channel<T>` is the coroutine equivalent of a `BlockingQueue`: one coroutine puts elements in, another takes them out.…
Start lesson - Step 30
Capacity and overflow policy
A channel's capacity is not a tuning knob, it is a semantic choice. `Channel.RENDEZVOUS` (capacity 0, the default) means…
Start lesson - Step 31
Fan-out and fan-in
Once you accept that every element on a channel has exactly one owner, the worker pool writes itself. **Fan-out** is sev…
Start lesson - Step 32
Pipelines and produce
A pipeline is a chain of coroutines where each stage consumes one channel and produces another. Stage 1 reads rows, stag…
Start lesson - Step 33
Actors: state with an inbox
An actor is a coroutine that owns some state and receives messages through a `Channel`. Nothing outside can reach the st…
Start lesson - Step 34
Capstone: confinement — state with exactly one owner
Part IV ends on the pattern that channels make possible and that most teams underuse. Instead of protecting shared state…
Start lesson
Cold asynchronous streams: the operator set, terminal operators, flowOn and context preservation, back-pressure, and writing your own operator.
- Step 35
Flow: the cold asynchronous stream
A `suspend fun` returns one value. A `Channel` is a hot queue with one owner per element. `Flow<T>` is the third shape: …
Start lesson - Step 36
Operators: transforming a stream
Operators are the vocabulary, and they divide cleanly into two kinds. **Intermediate** operators — `map`, `filter`, `tra…
Start lesson - Step 37
Terminal operators: the only things that make a flow run
A `Flow` built from `flow { }`, `map`, `filter` and `take` has done nothing at all. Those are **intermediate** operators…
Start lesson - Step 38
flowOn and the context preservation rule
Flow has one structural rule and it is enforced at runtime: **`emit` must happen in the same coroutine context the colle…
Start lesson - Step 39
Sequential by default: buffer, conflate, collectLatest
The most important property of a `Flow` is the least advertised: it is **sequential**. `emit(x)` is a suspending call th…
Start lesson - Step 40
The Flow contract and declarative error handling
The `flow { }` builder enforces two invariants, and both failures are famous enough to be worth meeting deliberately rat…
Start lesson - Step 41
Capstone: write your own operator
There is no operator registry in Kotlin. `map`, `filter`, `debounce` and every other operator is an ordinary extension f…
Start lesson
StateFlow vs SharedFlow, shareIn lifecycles, combining streams, debounce and sample — and how to choose between Channel, Flow and StateFlow.
- Step 42
StateFlow and SharedFlow
Everything in Part V was **cold**: a recipe, re-run per collector. Hot flows are the other half. They run **once**, inde…
Start lesson - Step 43
shareIn and stateIn: cold to hot
A cold `Flow` runs once per collector: two collectors mean two database queries, two websocket connections, two subscrip…
Start lesson - Step 44
Channel, Flow, SharedFlow, StateFlow: choosing
By this point in the course you have four ways to get a value from one coroutine to another, and the hardest part of usi…
Start lesson - Step 45
Combining flows: combine, zip, flatMapLatest
Once you have more than one stream you need to join them, and the three operators that do it are not interchangeable. Pi…
Start lesson - Step 46
debounce, sample and throttle
Some streams arrive faster than anything downstream should react to: keystrokes, mouse moves, sensor readings, a websock…
Start lesson - Step 47
Capstone: state, events and one shared upstream
Part VI has given you the three pieces separately. This capstone puts them in the arrangement they almost always appear …
Start lesson
runTest and the virtual clock — the part that turns timing from a source of flakiness into an assertion you can rely on.
- Step 48
runTest and the virtual clock
Every practice in this course has run inside `runTest`, and every timing claim has been exact. That is not a coincidence…
Start lesson - Step 49
Testing flows
Testing a **cold** flow is almost too easy, and that is the point of coldness. `toList()` inside `runTest` drives it to …
Start lesson - Step 50
Injecting dispatchers
Lesson 31 ended on the constraint that governs this whole part: **virtual time only applies to the test dispatcher.** Wo…
Start lesson - Step 51
Testing cancellation, timeouts and failures
Happy paths mostly test themselves. The paths that actually break in production — a cancelled request, an expired deadli…
Start lesson - Step 52
Testing channels and hot flows
Testing a cold flow is pleasant: `flow.toList()` gives you everything and you assert on a list. Hot streams take that aw…
Start lesson - Step 53
Capstone: pin a timing-sensitive component
Part III built a resilient call: bounded, timed and retried. It is exactly the kind of code that rots, because everyone …
Start lesson
Naming coroutines so a stack trace means something, finding the one that hung, and measuring what suspension actually costs.
- Step 54
Making a coroutine identifiable
The first problem with debugging coroutines is that they have no identity. A thread has a name and shows up in every dum…
Start lesson - Step 55
Finding the coroutine that stopped
This is the single biggest operational difference between threads and coroutines. A hung thread is *visible*: it is in e…
Start lesson - Step 56
What a coroutine actually costs
"Coroutines are cheap" is true and useless. Cheap compared to what, and which part? There are three distinct costs, they…
Start lesson - Step 57
Paying for concurrency you are not using
The costs from the previous lesson are only interesting where they are *wasted*. Two patterns waste them systematically,…
Start lesson - Step 58
Measuring coroutine code honestly
There are two clocks in this course and they measure different things. **The virtual clock tells you when things happen;…
Start lesson
The other half of every real codebase: JDBC, files, callbacks and futures — wrapped so they neither block the wrong pool nor leak on cancellation.
- Step 59
Spotting blocking code
Coroutines are a story about not holding threads, and most of the JVM ecosystem predates it. JDBC blocks. `java.io` bloc…
Start lesson - Step 60
Wrapping blocking code correctly
You have found the blocking call and you cannot replace it. The correct wrapper is short, and every piece of it earns it…
Start lesson - Step 61
Turning a callback into a suspend function
Every callback API has the same shape: you hand it a function, it calls that function later, on a thread of its choosing…
Start lesson - Step 62
Futures in, futures out, and streams of callbacks
Most of the bridges you need already exist. `kotlinx-coroutines-jdk8` supplies `CompletableFuture.await()` for a future …
Start lesson - Step 63
Transactions, connections and coroutine affinity
A database transaction lives on **one connection**. `BEGIN`, every statement, and `COMMIT` must all travel down the same…
Start lesson
Reactive Streams from first principles — demand and back-pressure — then Mono/Flux to suspend/Flow and back, without mixing the two styles.
- Step 64
Reactive Streams: four methods and one number
Reactive Streams is a specification, not a library — four interfaces that RxJava, Reactor, Akka Streams and the JDK's `F…
Start lesson - Step 65
Mono and Flux, coming in
Reactor types arriving at your code — from Spring's `WebClient`, from R2DBC, from the reactive Mongo driver — become sus…
Start lesson - Step 66
Suspend functions, going out
The other direction is the one that lets you adopt coroutines inside an application whose framework, callers, or half of…
Start lesson - Step 67
Back-pressure across the boundary
Both worlds solve the same problem — stopping a fast producer from burying a slow consumer — and they express it differe…
Start lesson - Step 68
One style per layer
Because the conversions are one-liners, it is easy to sprinkle them through a codebase until every layer speaks both lan…
Start lesson
Suspend controllers, coroutine services and repositories, request-scoped cancellation, and parallel downstream calls that respect a deadline.
- Step 69
Suspending controllers
Spring WebFlux has understood coroutines natively since 5.2. A controller method may be a **`suspend fun`** returning an…
Start lesson - Step 70
Layering: where the framework stops
The layering question is the one from Part XII's last lesson, made concrete. **Which layer is allowed to mention `Mono`,…
Start lesson - Step 71
When the client hangs up
A user closes the tab. A load balancer times out. A mobile client loses signal. In a thread-per-request server this prod…
Start lesson - Step 72
Parallel downstream calls with a deadline
An aggregating endpoint is the most common shape in a service-oriented backend: gather from several dependencies, assemb…
Start lesson - Step 73
Scopes, dispatchers and shutdown as configuration
Most coroutine code in a Spring application needs no configuration at all — a suspending controller calling a suspending…
Start lesson
Reactive persistence and brokers through a coroutine lens: connection-pool limits, transactional boundaries, consumer concurrency and idempotency.
- Step 74
Streaming a result set instead of holding it
Reactive drivers — R2DBC, the reactive Mongo driver, a paging HTTP API — all expose the same shape underneath: a **curso…
Start lesson - Step 75
Batching writes, and where to stop
Writing rows one at a time pays the round trip every time. Two hundred inserts at 10 ms of latency each is **2,000 ms**,…
Start lesson - Step 76
Consuming a broker: the poll loop as a Flow
Every message broker's consumer is the same loop: poll for a batch, process it, record how far you got. Kafka calls the …
Start lesson - Step 77
Ordering and consumer concurrency
A message consumer has two demands that pull against each other: **process faster** and **process in order**. Handling o…
Start lesson - Step 78
Idempotency, retries and dead letters
The previous lesson left you with a guarantee that duplicates are certain. This one makes that harmless. **An idempotent…
Start lesson
Retry with backoff and jitter, circuit breakers, bulkheads, fallbacks and deadline propagation — written as suspending functions, not annotations.
- Step 79
Retry, back-off and jitter
Retrying is the cheapest resilience there is and the easiest to get catastrophically wrong. A retry sends *more* traffic…
Start lesson - Step 80
The circuit breaker
Retrying helps a dependency that is briefly unwell. It actively harms one that is *down*: every call waits for a timeout…
Start lesson - Step 81
Bulkheads: one dependency's bad day
A ship's bulkhead does not stop a hull breach; it stops the breach from flooding the whole ship. The pattern is the same…
Start lesson - Step 82
Fallbacks and graceful degradation
Failing fast is only half a strategy. The other half is deciding what the user sees when a dependency is unavailable — a…
Start lesson - Step 83
Deadlines that shrink
Part XIII established the rule: one budget at the top, carried down by cancellation. This lesson is the version for a ca…
Start lesson
Designing for load: worker pools, batching and chunking, parallel pipelines, and choosing where the back-pressure is allowed to build up.
- Step 84
Worker pools and draining
The worker pool is the oldest concurrency shape there is and the simplest one to build with coroutines: a `Channel` for …
Start lesson - Step 85
Long jobs, checkpoints and resume
A batch that takes six hours will be interrupted: a deploy, a pod eviction, an OOM, a bad row at 80%. Without a checkpoi…
Start lesson - Step 86
Pipelines and the bottleneck
A multi-stage pipeline — fetch, parse, write — built from channels and worker pools is one of the most useful shapes in …
Start lesson - Step 87
Where the queue lives
When arrival rate exceeds service rate, a queue forms. That is arithmetic, not a design flaw — the only design decision …
Start lesson - Step 88
Rate limits are not concurrency limits
Two limits sound alike and are unrelated. **Concurrency** is how many calls may be *in flight* at once — a `Semaphore(5)…
Start lesson
Continuation, COROUTINE_SUSPENDED and the state labels the compiler emits — then the scheduler underneath: workers, local queues and work stealing.
- Step 89
Continuation and COROUTINE_SUSPENDED
Part I said `suspend` compiles to a state machine. This part takes the lid off, and the whole mechanism turns out to be …
Start lesson - Step 90
The state machine: labels and spilled locals
A suspending function with more than one suspension point compiles to **a class**. The class implements `Continuation`, …
Start lesson - Step 91
Interception: how a dispatcher gets its say
The previous two lessons left a gap: `resumeWith` is an ordinary method call, so how does a coroutine end up on a *parti…
Start lesson - Step 92
The scheduler underneath
`Dispatchers.Default` and `Dispatchers.IO` are two views of a single `CoroutineScheduler`. The thread names give it away…
Start lesson - Step 93
Local queues and work stealing
The previous lesson left the scheduler with per-worker local queues and a global one. That design has an obvious failure…
Start lesson
The layer coroutines are built on: threads, executors, ForkJoinPool, atomics and compare-and-set, and the happens-before rules that make state visible.
- Step 94
Threads, pools and what coroutines replaced
Coroutines run on threads, so it is worth being precise about what a thread costs and which of those costs coroutines ac…
Start lesson - Step 95
ForkJoinPool, parallel streams and the shared-pool trap
`ForkJoinPool` is the JDK's work-stealing scheduler, and Part XVII's local queues, LIFO ordering and stealing are the sa…
Start lesson - Step 96
Atomics and compare-and-set
**Compare-and-set is the hardware primitive underneath every lock-free structure**: "if this location still holds the va…
Start lesson - Step 97
Happens-before and visibility
**A write on one thread is not automatically visible on another.** The compiler may hoist a field read out of a loop, th…
Start lesson - Step 98
Coroutine-safe is not thread-safe
**Thread-safe** means: correct when two threads touch it at the same instant. **Coroutine-safe** means: correct when one…
Start lesson
Where scopes belong in a codebase, which layers may own one, the leaks that follow when they do not — and the review checklist for all of it.
- Step 99
Leaks, scopes and runBlocking
Coroutine leaks come in two shapes, and both are easy to see once you know the test to apply: **can you name the lifetim…
Start lesson - Step 100
Where a scope belongs
The single most useful architectural rule in this whole course fits on one line: **a class may own a `CoroutineScope` if…
Start lesson - Step 101
Request scope and application scope
A server has at least two lifetimes running at once. The **request scope** begins when a request arrives and ends when t…
Start lesson - Step 102
Suspend functions are the boundary
Once components own scopes and everything else suspends, one question remains: **what may cross a boundary between two l…
Start lesson - Step 103
The footgun checklist
Everything in this course has been building toward a practical ability: reading a diff and seeing the concurrency bug in…
Start lesson
Logging with correlation ids across suspension points, metrics that mean something, and shutting down without dropping work in flight.
- Step 104
Logging that survives a suspension point
Every JVM logging setup you have used carries request context in a **`ThreadLocal`** — SLF4J's MDC, Logback's `%X{reques…
Start lesson - Step 105
Metrics that mean something
You cannot operate what you cannot see, and a coroutine service is *less* visible than a thread-per-request one — thread…
Start lesson - Step 106
Graceful shutdown
`scope.cancel()` is not a shutdown, it is an abort. Fifty jobs half-way through a 200 ms operation, cancelled the moment…
Start lesson - Step 107
The Kubernetes pod lifecycle
The previous lesson's three-move shutdown is correct and, dropped into Kubernetes unchanged, it will still return 502s o…
Start lesson - Step 108
Health checks that check the right thing
The default health endpoint returns `200 OK` from whichever thread served the request. In a coroutine service that is al…
Start lesson
When not to parallelize at all, where the real ceiling is, and how to pick a concurrency number you can defend with a measurement.
- Step 109
When not to parallelize
Twenty parts of this course have been about doing things concurrently. This one is about the cases where the honest answ…
Start lesson - Step 110
Where the ceiling actually is
"How many should I run in parallel?" is usually answered by doubling until it feels fast. There is a better way, and it …
Start lesson - Step 111
Choosing the number
The previous lesson gave you the tools — the knee, the retrograde curve, `L = λW`. This one is about applying them, and …
Start lesson - Step 112
When memory is the limit, not time
Every previous lesson in this part treated concurrency as a question about speed. It is also, and often first, a questio…
Start lesson
Six build-it-yourself systems that combine everything: an aggregator, a bounded scraper, a work queue, an event bus, a streaming pipeline, and a final review.
- Step 113
Lab 1: the fan-out aggregator
The first lab is the shape almost every service eventually has: a page assembled from several backends, none of which yo…
Start lesson - Step 114
Lab 2: the bounded scraper
The second lab is a client for a resource that has opinions: a host that answers four requests at a time, allows twenty …
Start lesson - Step 115
Lab 3: the work queue
A `Channel` plus `repeat(n) { launch { for (m in inbox) … } }` is a worker pool in four lines, and Part VI built it. Thi…
Start lesson - Step 116
Lab 4: the in-process event bus
An in-process event bus looks like the easiest thing in this part and contains the sharpest trade-off. Start with the ty…
Start lesson - Step 117
Lab 5: the streaming pipeline
The last build-it lab is a four-stage pipeline: read, parse, enrich, write, at 5, 10, 20 and 8 milliseconds an item. For…
Start lesson - Step 118
Final capstone: a component that survives review
Everything in this course was aiming at one artefact: a component you would be comfortable shipping and comfortable hand…
Start lesson
Want more reps? These Arena challenge banks drill the operators and patterns from this course.