Reactive Kotlin: Reactor, Coroutines & WebFlux
Learn the reactive Kotlin stack the way it actually appears in production, modelled on a real vehicle-inventory import service (VINs, prices in cents, dealer fees, ADD/UPDATE/DELETE deltas). 78 lessons across 8 chapters: Kotlin idioms → Project Reactor (Mono/Flux) → coroutines & interop → Spring WebFlux. Every challenge compiles and runs on Kotlin 1.8.20 with faithful teaching primitives (a hand-rolled Flux/Mono, a stdlib coroutine runner, a miniature DI container) — the semantics are real, the code runs.
Null safety & Elvis → data class + copy → default/named args → extension functions → scope functions → lambdas & higher-order functions → collection pipelines → when & smart casts → sealed types → the row→Vehicle converter capstone.
- Step 1
Null safety & the Elvis operator
Reactive inventory code is drowning in *maybe-present* values. A feed row might carry a driveway price, or only an askin…
Start lesson - Step 2
data class & .copy() — immutable merges
A `data class` is Kotlin's value-holder: declare the properties in the primary constructor and you get `equals`/`hashCod…
Start lesson - Step 3
Default & named arguments
Why does the `Vehicle` model have `= null` on almost every property? Because a feed row rarely fills them all, and **def…
Start lesson - Step 4
Extension functions
Prices live in the database as **cents** (an `Int`) but the API speaks **dollars** (a `Double`). Rather than scatter `ce…
Start lesson - Step 5
Scope functions — with / let / apply / also / run
The converter builds a `Vehicle` inside `with(tsvCar) { ... }` so it can read `vin`, `drivewayPrice`, `inventoryId` dire…
Start lesson - Step 6
Lambdas & higher-order functions
The converter doesn't know how to look up a dealer's free-shipping zip codes — and it shouldn't. Instead it accepts a **…
Start lesson - Step 7
Collections pipeline — map / filter / chunked / mapNotNull
The import reads thousands of rows and runs them through a *pipeline*: `rows.map { convert(it) }.chunked(500).map { batc…
Start lesson - Step 8
when & smart casts
Deriving a vehicle's condition is a textbook `when`: a `New` listing is `NEW`; a `Used` one is `CPO` if certified else `…
Start lesson - Step 9
Sealed types & exhaustive when
An inventory diff produces one of exactly three outcomes per VIN: an **ADD** (new to the feed), a **DELETE** (gone from …
Start lesson - Step 10
Capstone — the row → Vehicle converter
Time to assemble Chapter 1. The real `ExtractedRowToVehicleConverter` takes a raw feed row and produces a clean `Vehicle…
Start lesson
Cold vs hot & subscription → building Flux → building Mono → map vs flatMap → transform → then/thenReturn → switchIfEmpty → doOnNext/doOnError/doFinally → onErrorResume/retry → the upsert-merge pipeline capstone.
- Step 11
Why reactive? Cold streams & subscription
A reactive pipeline is a *recipe*, not a running computation. When you write `flux.map { ... }.filter { ... }`, nothing …
Start lesson - Step 12
Flux<T> — the 0..N stream & map
`Flux<T>` is the 0-to-many publisher: a feed of vehicles, a stream of rows, the results of a query. The simplest transfo…
Start lesson - Step 13
Mono<T> — the 0..1 value
`Mono<T>` is the 0-or-1 publisher: the result of `getByVin(vin)`, a single upsert, a count. It's `Flux`'s sibling for "a…
Start lesson - Step 14
map vs flatMap
`map` transforms a value into another *value*. `flatMap` transforms a value into another *publisher* and flattens the re…
Start lesson - Step 15
transform — reusable pipeline segments
When the same chain of operators appears in three pipelines — "keep only searchable vehicles, in dollars" — you don't co…
Start lesson - Step 16
then — sequencing stages
The import job is a sequence of stages: validate the file, *then* import to a temp collection, *then* publish deltas, *t…
Start lesson - Step 17
switchIfEmpty — fallback when nothing emits
An empty `Mono` is a first-class outcome: `getCounts()` might find no document, `getByVin` might miss. `switchIfEmpty(fa…
Start lesson - Step 18
doOnNext / doFinally — lifecycle side-effects
You can't pepper a reactive chain with `println` the way you would a loop — there's no loop body to put it in. Instead, …
Start lesson - Step 19
onErrorResume — fallback on failure
Feeds are messy: a bad row throws mid-stream. `onErrorResume(fn)` switches to an entirely different publisher when the s…
Start lesson - Step 20
Capstone — the upsert-merge pipeline
This is the heart of the import, in miniature. The real pipeline reads `tempCollection.find()` as a `Flux<Vehicle>`, mat…
Start lesson
suspend & suspend main → a stdlib coroutine runner → the mono{}/flux{} bridge → awaitSingle/awaitFirst → structured concurrency → Semaphore-bounded parallelism → chunked batches → withTimeout → cancellation → the coDoVehicleImport capstone.
- Step 21
suspend functions & suspend fun main
A `suspend` function is one that can *pause* and resume later without blocking a thread. You can only call a suspend fun…
Start lesson - Step 22
Building a coroutine runner from the stdlib
How does a *non-suspend* `main` (or a Spring `@Scheduled` method) actually run a suspend function? Something has to *dri…
Start lesson - Step 23
The mono { } bridge — coroutine → reactive
The cron does `fun doVehicleImport(...): Mono<...> = mono { coDoVehicleImport(...) }` — it writes the import as a clean …
Start lesson - Step 24
awaitSingle — reactive → coroutine
The bridge runs both ways. Inside suspend code you often need a value *out* of a `Mono`: the real handler does `vehicleD…
Start lesson - Step 25
Structured concurrency — awaitAll
Structured concurrency means children never outlive their parent scope: launch several jobs, and the scope doesn't retur…
Start lesson - Step 26
Bounded parallelism — Semaphore & chunked batches
You can't fan out 50,000 DB writes at once — you'll exhaust connections and memory. The importer bounds concurrency two …
Start lesson - Step 27
withTimeout — bounding a stage
A stage that hangs forever is worse than one that fails. The importer wraps its temp-collection cleanup in `withTimeout(…
Start lesson - Step 28
Cooperative cancellation
Cancellation in coroutines is *cooperative*: nobody forcibly kills your code — a flag flips, and well-behaved code check…
Start lesson - Step 29
coroutineScope vs supervisorScope
Two scoping policies decide what happens when a child fails. `coroutineScope` is **fail-fast**: if any child throws, the…
Start lesson - Step 30
Capstone — the coDoVehicleImport orchestration
Chapter 3 in one program. The real `coDoVehicleImport` is a suspend function that batches incoming rows, upserts each ba…
Start lesson
Dependency injection → @Component/@Service/@Configuration → constructor injection → @ConfigurationProperties ↔ application.yml → @Scheduled crons → WebFlux vs MVC → reactive DAOs → end-to-end wiring → observability & checkpoints → the full EDS reader job capstone.
- Step 31
Dependency injection — why classes just get their deps
Open any Spring class and the dependencies arrive as constructor parameters — `class VehicleService(val dao: VehicleDao)…
Start lesson - Step 32
@Component / @Service & the container
Spring's `@Component`/`@Service`/`@Configuration` annotations register a class as a *bean* in the application context — …
Start lesson - Step 33
Constructor injection — wiring a component
`CronEdsVehicleImporter` takes a dozen collaborators in its constructor — DAO, fee client, file storage, converters. Spr…
Start lesson - Step 34
@ConfigurationProperties ↔ application.yml
Cron timings, batch sizes, collection names — none of that belongs in code. Spring binds `application.yml` keys onto a t…
Start lesson - Step 35
@Scheduled crons
`@Scheduled(cron = "${cron.vehicleReader.cronTiming}")` on a method tells Spring to call it on a timer — that's how the …
Start lesson - Step 36
WebFlux — the reactive web stack
Spring MVC is *servlet* (blocking): a handler returns a `String`/object, and one thread is tied up per request until it …
Start lesson - Step 37
Reactive DAOs — Mono & Flux at the data layer
A reactive repository never returns a `Vehicle` — it returns a `Mono<Vehicle>` (or `Flux<Vehicle>`). `getByVin` returns …
Start lesson - Step 38
End-to-end wiring — DAO → Service → Controller
Now assemble the layers the way the application context does at boot: a `VehicleDao` (data), injected into a `VehicleSer…
Start lesson - Step 39
Observability — checkpoints & metrics
A long reactive pipeline is hard to debug — there's no stack trace through lazy operators. Reactor's `.checkpoint("Stage…
Start lesson - Step 40
Capstone — the full EDS reader job
The finale: the staged import job, modelled end to end. The real `EdsVehicleReaderV2` threads an `InventoryJobDetails` t…
Start lesson
collectList → buffer batches → zipWith & Tuple2 record pairing → combineRecords nested-copy merge → limitRate & backpressure → retryWhen → distinctUntilChanged → set-based delta calculation → Schedulers (publishOn/subscribeOn) → the record-pair merge job capstone.
- Step 41
collectList — Flux → Mono<List>
Sometimes you need the *whole* stream as one list — to bulk-write it, count it, or diff it. `collectList()` turns a `Flu…
Start lesson - Step 42
buffer — fixed-size batches
`collectList` gathers *everything*; `buffer(n)` gathers in *chunks*. It accumulates `n` elements, emits them as a `List`…
Start lesson - Step 43
zipWith & Tuple2 — pairing incoming with existing
The merge starts by *pairing* each incoming vehicle with its existing DB record. The real `matchRecordsFor` does `Mono.j…
Start lesson - Step 44
combineRecords — the nested-copy merge
Now the heart of the merge. Given the `Tuple2(incoming, existing)` pair, `combineRecords` produces the record to persist…
Start lesson - Step 45
limitRate & backpressure
Backpressure is the reactive answer to "the producer is faster than the consumer." A subscriber signals *demand* — "send…
Start lesson - Step 46
retryWhen — bounded retries
Transient failures — a dropped connection, a throttled API — shouldn't kill a whole job. `retryWhen` re-subscribes to a …
Start lesson - Step 47
distinctUntilChanged — suppress consecutive duplicates
`distinctUntilChanged()` forwards a value only when it differs from the *previous* one — it collapses runs of duplicates…
Start lesson - Step 48
Set-based delta calculation
How does the import know what changed? It diffs two sets of VINs. The real `getInventoryDeltas` computes `adds = incomin…
Start lesson - Step 49
Schedulers — publishOn / subscribeOn
Reactive code is non-blocking, but *blocking* work (a JDBC call, file I/O) still has to run somewhere safe. **Schedulers…
Start lesson - Step 50
Capstone — the record-pair merge job
The grand finale: the merge job that pairs every incoming vehicle with its existing record, combines them (carrying stic…
Start lesson
Cold Flow built from the stdlib → Flow operators (map, take) → StateFlow/SharedFlow → Flow vs Flux & interop → @JvmInline value classes → sealed interface → Result & runCatching → fun interface (SAM) → a modern Flow-based import capstone.
- Step 51
Flow<T> — the coroutine-native cold stream
Reactor predates Kotlin coroutines; the modern, coroutine-native answer to `Flux` is `Flow<T>`. A `Flow` is a *cold* asy…
Start lesson - Step 52
Flow operators — map via flow { collect { emit } }
Flow operators have a beautiful self-similar shape: each one is *itself* a `flow { }` that collects the upstream and emi…
Start lesson - Step 53
Flow.take — bounding a stream
`take(n)` keeps only the first `n` values then stops caring about the rest — the bounded-prefix operator. On an unbounde…
Start lesson - Step 54
StateFlow & SharedFlow — hot state
A plain `Flow` is *cold* — each collector re-runs the producer. Sometimes you want the opposite: one shared, always-curr…
Start lesson - Step 55
Flow vs Flux — terminal operators & interop
`Flow` and `Flux` solve the same problem from two eras. `Flux` (Reactor) is subscription-based, framework-agnostic, and …
Start lesson - Step 56
value classes — type-safe ids, zero cost
A VIN is a `String`; a price is an `Int` — until you accidentally pass a zip code where a VIN belongs and the compiler s…
Start lesson - Step 57
sealed interface — modern result hierarchies
Chapter 1 used a `sealed class`; modern Kotlin often prefers a `sealed interface` for the same closed-set guarantee with…
Start lesson - Step 58
Result & runCatching — functional error handling
Exceptions are control flow you can't see in a type signature. Kotlin's `Result<T>` makes success-or-failure a *value*: …
Start lesson - Step 59
fun interface — SAM conversions
When you want a *named* single-method abstraction you can satisfy with a lambda, mark it `fun interface` (a functional i…
Start lesson - Step 60
Capstone — a modern Flow-based import
The course closes where modern Kotlin would start: the import, rewritten with a `Flow` of rows and a `sealed interface` …
Start lesson
Want more reps? These Arena challenge banks drill the operators and patterns from this course.