Kotlin reactivo: Reactor, corrutinas y WebFlux
Aprende el stack reactivo de Kotlin como aparece en producción, modelado sobre un servicio real de importación de inventario de vehículos (VINs, precios en centavos, tarifas de dealer, deltas ADD/UPDATE/DELETE). 78 lecciones en 8 capítulos: idioms de Kotlin → Project Reactor (Mono/Flux) → corrutinas e interop → Spring WebFlux. Cada reto compila y corre en Kotlin 1.8.20 con primitivas didácticas (un Flux/Mono hecho a mano, un runner de corrutinas stdlib y un contenedor DI en miniatura) — la semántica es real, el código corre.
Null safety y Elvis → data class + copy → argumentos por defecto/nombrados → funciones de extensión → scope functions → lambdas y funciones de orden superior → pipelines de colecciones → when y smart casts → tipos sealed → capstone del converter fila→Vehicle.
- Paso 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…
Empezar lección - Paso 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…
Empezar lección - Paso 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…
Empezar lección - Paso 4
Extension functions
Prices live in the database as **cents** (an `Int`) but the API speaks **dollars** (a `Double`). Rather than scatter `ce…
Empezar lección - Paso 5
Scope functions — with / let / apply / also / run
The converter builds a `Vehicle` inside `with(tsvCar) { ... }` so it can read `vin`, `drivewayPrice`, `inventoryId` dire…
Empezar lección - Paso 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 **…
Empezar lección - Paso 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…
Empezar lección - Paso 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 `…
Empezar lección - Paso 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 …
Empezar lección - Paso 10
Capstone — the row → Vehicle converter
Time to assemble Chapter 1. The real `ExtractedRowToVehicleConverter` takes a raw feed row and produces a clean `Vehicle…
Empezar lección
Cold vs hot y suscripción → construir Flux → construir Mono → map vs flatMap → transform → then/thenReturn → switchIfEmpty → doOnNext/doOnError/doFinally → onErrorResume/retry → capstone del pipeline upsert-merge.
- Paso 11
Why reactive? Cold streams & subscription
A reactive pipeline is a *recipe*, not a running computation. When you write `flux.map { ... }.filter { ... }`, nothing …
Empezar lección - Paso 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…
Empezar lección - Paso 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…
Empezar lección - Paso 14
map vs flatMap
`map` transforms a value into another *value*. `flatMap` transforms a value into another *publisher* and flattens the re…
Empezar lección - Paso 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…
Empezar lección - Paso 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…
Empezar lección - Paso 17
switchIfEmpty — fallback when nothing emits
An empty `Mono` is a first-class outcome: `getCounts()` might find no document, `getByVin` might miss. `switchIfEmpty(fa…
Empezar lección - Paso 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, …
Empezar lección - Paso 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…
Empezar lección - Paso 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…
Empezar lección
suspend y suspend main → un runner de corrutinas stdlib → el puente mono{}/flux{} → awaitSingle/awaitFirst → concurrencia estructurada → paralelismo acotado con Semaphore → batches con chunked → withTimeout → cancelación → capstone coDoVehicleImport.
- Paso 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…
Empezar lección - Paso 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…
Empezar lección - Paso 23
The mono { } bridge — coroutine → reactive
The cron does `fun doVehicleImport(...): Mono<...> = mono { coDoVehicleImport(...) }` — it writes the import as a clean …
Empezar lección - Paso 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…
Empezar lección - Paso 25
Structured concurrency — awaitAll
Structured concurrency means children never outlive their parent scope: launch several jobs, and the scope doesn't retur…
Empezar lección - Paso 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 …
Empezar lección - Paso 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(…
Empezar lección - Paso 28
Cooperative cancellation
Cancellation in coroutines is *cooperative*: nobody forcibly kills your code — a flag flips, and well-behaved code check…
Empezar lección - Paso 29
coroutineScope vs supervisorScope
Two scoping policies decide what happens when a child fails. `coroutineScope` is **fail-fast**: if any child throws, the…
Empezar lección - Paso 30
Capstone — the coDoVehicleImport orchestration
Chapter 3 in one program. The real `coDoVehicleImport` is a suspend function that batches incoming rows, upserts each ba…
Empezar lección
Inyección de dependencias → @Component/@Service/@Configuration → inyección por constructor → @ConfigurationProperties ↔ application.yml → crons @Scheduled → WebFlux vs MVC → DAOs reactivos → wiring de extremo a extremo → observabilidad y checkpoints → capstone del job lector EDS completo.
- Paso 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)…
Empezar lección - Paso 32
@Component / @Service & the container
Spring's `@Component`/`@Service`/`@Configuration` annotations register a class as a *bean* in the application context — …
Empezar lección - Paso 33
Constructor injection — wiring a component
`CronEdsVehicleImporter` takes a dozen collaborators in its constructor — DAO, fee client, file storage, converters. Spr…
Empezar lección - Paso 34
@ConfigurationProperties ↔ application.yml
Cron timings, batch sizes, collection names — none of that belongs in code. Spring binds `application.yml` keys onto a t…
Empezar lección - Paso 35
@Scheduled crons
`@Scheduled(cron = "${cron.vehicleReader.cronTiming}")` on a method tells Spring to call it on a timer — that's how the …
Empezar lección - Paso 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 …
Empezar lección - Paso 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 …
Empezar lección - Paso 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…
Empezar lección - Paso 39
Observability — checkpoints & metrics
A long reactive pipeline is hard to debug — there's no stack trace through lazy operators. Reactor's `.checkpoint("Stage…
Empezar lección - Paso 40
Capstone — the full EDS reader job
The finale: the staged import job, modelled end to end. The real `EdsVehicleReaderV2` threads an `InventoryJobDetails` t…
Empezar lección
collectList → batches con buffer → emparejado con zipWith & Tuple2 → merge combineRecords con copy anidado → limitRate y backpressure → retryWhen → distinctUntilChanged → cálculo de deltas con conjuntos → Schedulers (publishOn/subscribeOn) → capstone del job de merge por pares.
- Paso 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…
Empezar lección - Paso 42
buffer — fixed-size batches
`collectList` gathers *everything*; `buffer(n)` gathers in *chunks*. It accumulates `n` elements, emits them as a `List`…
Empezar lección - Paso 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…
Empezar lección - Paso 44
combineRecords — the nested-copy merge
Now the heart of the merge. Given the `Tuple2(incoming, existing)` pair, `combineRecords` produces the record to persist…
Empezar lección - Paso 45
limitRate & backpressure
Backpressure is the reactive answer to "the producer is faster than the consumer." A subscriber signals *demand* — "send…
Empezar lección - Paso 46
retryWhen — bounded retries
Transient failures — a dropped connection, a throttled API — shouldn't kill a whole job. `retryWhen` re-subscribes to a …
Empezar lección - Paso 47
distinctUntilChanged — suppress consecutive duplicates
`distinctUntilChanged()` forwards a value only when it differs from the *previous* one — it collapses runs of duplicates…
Empezar lección - Paso 48
Set-based delta calculation
How does the import know what changed? It diffs two sets of VINs. The real `getInventoryDeltas` computes `adds = incomin…
Empezar lección - Paso 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…
Empezar lección - Paso 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…
Empezar lección
Flow frío construido desde la stdlib → operadores de Flow (map, take) → StateFlow/SharedFlow → Flow vs Flux e interop → value classes @JvmInline → sealed interface → Result y runCatching → fun interface (SAM) → capstone de importación moderna basada en Flow.
- Paso 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…
Empezar lección - Paso 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…
Empezar lección - Paso 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…
Empezar lección - Paso 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…
Empezar lección - Paso 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 …
Empezar lección - Paso 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…
Empezar lección - Paso 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…
Empezar lección - Paso 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*: …
Empezar lección - Paso 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…
Empezar lección - Paso 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` …
Empezar lección
¿Quieres más repeticiones? Estos bancos de retos en Arena profundizan los operadores y patrones de este curso.