Fire-and-Forget with Coroutines
launch fires background work and returns a Job; async returns a Deferred you must await, and the Mongo write needs the first, not the second.
Module 3 ended with a decision: the write to `valuation_event` in MongoDB should not sit on the request's critical path. `VehicleController` calls `VehicleValuationService.valuate()`, gets a `ValuationResult`, and responds to the client — the audit write should happen, but it has no business making the caller wait for it. Coroutines give you exactly two ways to start new concurrent work from a `CoroutineScope`, and picking the wrong one either defeats the whole point or hides a bug you won't see until production.
`launch` starts a coroutine and returns a `Job` — a handle to that coroutine's lifecycle. You can call `job.join()` to suspend until it finishes, `job.cancel()` to stop it early, or inspect `isActive` / `isCompleted`, but a `Job` carries no return value. Whatever the block computes is thrown away unless the block itself does something with it — writes to a database, logs a line, increments a counter. That's precisely the shape of fire-and-forget: start the work, keep a handle on it if you need one, and move on without waiting for an answer.
`async` starts a coroutine too, but returns a `Deferred<T>` — a `Job` that also promises a future result. You retrieve that result by calling `.await()`, which suspends the caller until the block finishes and either hands back the value or rethrows whatever exception the block threw. `async` models a different question: 'I need this answer, and I want to start computing several answers concurrently.' That's the shape of calling Black Book, Carfax, and S&P VIS at the same time and awaiting all three before the rules engine can run.
The `valuation_event` write has no answer anyone is waiting for. Once `valuate()` has computed a `ValuationResult`, the audit write is a pure side effect — nothing downstream reads a value out of it. That makes it a `launch`, not an `async`. Reaching for `async` here and never calling `.await()` isn't just the wrong tool, it's actively dangerous: an `async` coroutine that fails does not report that failure anywhere on its own — the exception sits inside the `Deferred`, waiting for an `.await()` that will never come, and it is never seen. A `launch` at least has somewhere to go when it fails, which is the subject of the next lesson.
Here's the trap that catches people who get the builder right and still ship a bug: the scope you `launch` on matters as much as the builder you call. Coroutines are structured — every one is a child of a scope, and a child cannot outlive its parent's `Job`. If the request handler runs inside a scope whose lifetime is tied to the HTTP request — cancelled the moment the response is written, which is exactly how several coroutine-aware web layers behave — and you `launch` the Mongo write on that same scope, cancellation reaches your write the instant the response leaves. The caller sees a fast, successful response. The audit trail quietly loses an entry. And because your local tests run fast enough that the write usually finishes before the response does, this bug hides until it's running under real production latency.
The fix is a scope whose lifetime is decoupled from any single request — specifically, one that outlives it. In practice that means the service owns a long-lived `CoroutineScope`, created once and reused by every call to `valuate()`, rather than a scope born and torn down alongside the request that triggered the write. A request can finish, time out, or even throw, and the `launch`ed write keeps running, because it is a child of the service's scope, not the request's. Lesson 17 wires exactly this into `VehicleValuationService`. For now, hold onto the rule: `launch` is fire-and-forget, but the scope you `launch` on decides how long 'fire' survives after you've 'forgotten' it.
One question is still open, and it matters the moment this code meets a flaky network: what happens when the `launch`ed block throws? By default, a failing child coroutine does not fail quietly — it cancels its siblings and its parent too. For a scope shared by many concurrent, unrelated valuation requests, that default is exactly backwards. Fixing it is the next lesson's job.
import kotlinx.coroutines.*suspend fun writeAuditEvent(vin: String) {delay(50) // stands in for a real MongoDB insertprintln("Audit event persisted for VIN $vin")}fun main() = runBlocking {// A scope that lives independently of any single requestval serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)println("Valuation computed, sending the response to the client now")serviceScope.launch {writeAuditEvent("1HGCM82633A004352")}println("Response sent")delay(100) // only here so main() can observe the write finish before the program exitsserviceScope.cancel()}
A fire-and-forget write on a scope that outlives the request: the response goes out immediately, and the write keeps running afterward.
Arena IDEimport kotlinx.coroutines.*suspend fun writeAuditEvent(vin: String) {delay(200)println("Audit event persisted for VIN $vin") // this line should never print below}fun main() = runBlocking {// Simulates a scope whose lifetime is tied to the HTTP request itselfval requestScope = CoroutineScope(Job() + Dispatchers.Default)requestScope.launch {writeAuditEvent("1HGCM82633A004352")}println("Response sent to client")requestScope.cancel() // the framework tears the request scope down right after the response is writtendelay(300)println("Notice the audit write above never got the chance to finish")}
The trap: launching on a scope tied to the request's own lifetime gets the write cancelled the moment that scope is torn down, right after the response is sent.
Arena IDE// launch: fire-and-forget — returns a Job, no result to readval auditWrite: Job = valuationEventScope.launch {valuationEventRepository.save(event)}// async: needs a result — returns a Deferred<T> you retrieve with await()val blackBook: Deferred<BlackBookValuation> = scope.async {blackBookClient.getValuation(vin)}val valuation = blackBook.await()
launch and async side by side: the audit write needs the first, the provider calls need the second.
🧠 Check your understanding
0/1 · 0/1 answered1. VehicleValuationService needs to persist a valuation_event to MongoDB after computing a valuation, but the HTTP response should not wait for that write to finish. Which coroutine builder should start the write, and why?