What is structured concurrency in Kotlin coroutines, and what core guarantee does it give you?
answer
- Every coroutine has a parent
- Parent waits for all children
- Cancellation flows down, failure flows up
- coroutineScope { } suspends until children done
- GlobalScope = unstructured = leak
basics
~10 sEvery coroutine runs inside a scope, and that scope won't finish until all the coroutines it started have finished. This stops background work from being forgotten or leaking.
solid answer
~40 sStructured concurrency means every coroutine is launched inside a CoroutineScope and becomes a child of that scope's Job. A scope (or a suspending coroutineScope { } block) cannot complete until all of its children complete. This binds the lifetime of background work to a well-defined parent, so coroutines can't be silently orphaned or leaked. It also gives automatic propagation: cancelling the scope cancels every child, and (in non-supervised scopes) a child failure cancels its siblings and the parent. You launch via builders like launch and async on a scope, and you create temporary scopes with coroutineScope { }. The opposite — calling GlobalScope.launch — breaks the principle because the work is tied to the whole process, not to a caller, so nobody waits for it or cancels it.
code
kotlin · 8 linesimport kotlinx.coroutines.*
suspend fun work() = coroutineScope {
launch { delay(100); println("child A done") }
launch { delay(50); println("child B done") }
println("block body finished, but coroutineScope still waits")
}
// coroutineScope returns ONLY after both children printgo deeper
Can state the rule: coroutines live in a scope, the scope waits for its children, and this prevents leaks.
Adds that launch/async are scope extensions, that coroutineScope suspends until children finish, and that cancellation/failure propagate.
Frames it as lifetime ownership, contrasts with GlobalScope, and explains the parent 'completing' state and downward cancel / upward failure propagation.
Discusses how the guarantee shapes API design (suspend functions own their concurrency), error/cancellation semantics, and how it composes across module boundaries.
## The problem it solves With raw threads or callbacks, you can start background work and lose track of it: the caller returns, but the work keeps running with no owner. Nobody waits for it, nobody cancels it, and errors vanish. These are **leaked** or **orphaned** tasks. **Structured concurrency** is the rule that fixes this: *every coroutine has a parent, and a parent cannot complete until all its children have completed.* ## The building blocks - **Coroutine**: a suspendable unit of work, cheaper than a thread. - **CoroutineScope**: an object that owns coroutines. It carries a `CoroutineContext`, which includes a **Job** (the handle representing the lifecycle). - **Coroutine builders**: `launch { }` (fire-and-forget, returns a `Job`) and `async { }` (returns a `Deferred<T>` you `await()`). They are *extension functions on `CoroutineScope`* — you can only start a coroutine if you have a scope. - **`coroutineScope { }`**: a *suspending* function that creates a child scope, runs the block, and **suspends until every coroutine started inside it finishes** before returning. ## The guarantee When you `launch` inside a scope, the new coroutine's `Job` becomes a **child** of the scope's `Job`. The parent stays in a "completing" state until all children are done. Three consequences follow: 1. **No leaks** — work is bound to a caller's lifetime; when the scope ends, so does the work. 2. **Cancellation propagates down** — cancelling the parent cancels all children. 3. **Failures propagate up** — in an ordinary (non-supervisor) scope, an uncaught child failure cancels siblings and the parent. ```kotlin import kotlinx.coroutines.* suspend fun loadDashboard(): Pair<User, List<Order>> = coroutineScope { val user = async { fetchUser() } // child 1 val orders = async { fetchOrders() } // child 2 // coroutineScope does NOT return until BOTH children finish user.await() to orders.await() } ``` If `fetchOrders()` throws, `coroutineScope` cancels `fetchUser()` too and rethrows — no half-finished work escapes. ## Breaking the principle `GlobalScope.launch { }` ties the coroutine to the whole application lifetime, not to a caller. Nobody awaits or cancels it, so it can leak. That is exactly the unstructured pattern structured concurrency exists to prevent; `GlobalScope` is marked `@DelicateApi`. ## Why it matters It makes concurrency *local and predictable*: you can read a function and know that when it returns, all the work it spawned is finished or cancelled — no dangling background tasks.
- Why is GlobalScope.launch discouraged?It ties the coroutine to the application lifetime, not to a caller, so no one waits for or cancels it — that's an orphaned/leaked coroutine, the exact opposite of structured concurrency.
- Name one builder that requires a CoroutineScope.launch and async are both extension functions on CoroutineScope, so you need a scope to call them.
Like a function that doesn't return until every helper thread it spawned has joined — the caller never leaves work running behind it.
saying these in an interview costs you the question
- Thinking coroutineScope { } returns as soon as the last line of the block runs, ignoring still-running children
- Believing GlobalScope is the normal way to start coroutines
- Confusing 'coroutine' with 'thread' (a coroutine is not a thread)
- Saying scopes are only about threading, not lifecycle/ownership
- Not knowing that launch returns a Job and async returns a Deferred