What is structured concurrency, and how does the parent Job/scope tie coroutine lifetimes together?
answer
- Coroutines form a parent-child Job tree
- Parent waits for all children to finish
- Cancel parent -> children cancelled (down)
- Child failure cancels parent+siblings (up) unless supervisor
- coroutineScope structured; GlobalScope unstructured/leaky
basics
~20 sStructured concurrency means every coroutine runs inside a scope, and a parent waits for all its children to finish. If the parent is cancelled, its children are cancelled too — so nothing is left running or leaked.
solid answer
~40 sStructured concurrency is the rule that coroutines form a **parent–child tree** anchored in a `CoroutineScope`, and a parent's lifetime contains its children's. When you `launch`/`async` inside a scope, the new coroutine's `Job` becomes a **child** of the scope's `Job`. Consequences: (1) the scope (or an enclosing `coroutineScope { }`) does not complete until **all children complete**; (2) **cancelling the parent cancels all children** (propagation downward); (3) by default, an **uncaught failure in a child cancels its parent and siblings** (propagation upward), unless you use a `SupervisorJob`/`supervisorScope`. This makes leaks structurally impossible — you can't accidentally start a coroutine that outlives its caller, and errors aren't silently swallowed. It is the model's safety mechanism: lifetimes are tied to the `Job` hierarchy rather than left dangling like raw threads.
code
kotlin · 11 linesimport kotlinx.coroutines.*
suspend fun process(): Int = coroutineScope {
val a = async { compute(1) }
val b = async { compute(2) }
// If compute(2) throws, 'a' is cancelled too, and process() rethrows.
// coroutineScope does not return until both children settle -> no leak.
a.await() + b.await()
}
suspend fun compute(x: Int): Int { delay(50); return x * 10 }go deeper
Knows coroutines live in a scope and cancelling the scope stops them.
Explains the parent-child Job tree, that the scope awaits all children, and downward cancellation.
Adds upward failure propagation, contrasts coroutineScope vs supervisorScope, and explains why GlobalScope breaks the model.
Designs scope hierarchies for lifecycles (request/screen scopes), reasons about supervisor boundaries and cancellation cleanup at scale.
## The core idea **Structured concurrency** says: a coroutine never escapes the scope that started it. Every coroutine has a parent, forming a **tree of `Job`s**, and a parent's lifetime **encloses** all of its children. The opposite — unstructured — is firing off background threads that may outlive their creator, leak, or fail silently. ## How the tree is built Everything runs inside a `CoroutineScope`, which carries a `CoroutineContext` containing a `Job`. When you call a builder: ```kotlin import kotlinx.coroutines.* suspend fun loadAll() = coroutineScope { // creates a child scope val a = async { fetchA() } // child Job of this scope val b = async { fetchB() } // child Job of this scope a.await() + b.await() } // does NOT return until BOTH children finish ``` `async`/`launch` create a new `Job` whose **parent** is the scope's `Job`. This parent–child link is what enforces the guarantees. ## The three guarantees 1. **Wait for children**: a `coroutineScope { }` block (and structured scopes generally) **suspends until every child completes**. The function won't return with work still running in the background. 2. **Cancellation flows down**: cancel the parent `Job` and all descendant coroutines are cancelled. One call tears down the whole subtree. 3. **Failure flows up (by default)**: if a child throws an uncaught exception, it cancels its parent, which cancels the **siblings** too. This surfaces errors instead of losing them. To stop a failing child from killing siblings, use `SupervisorJob` / `supervisorScope`, where children fail independently. ## Why it matters - **No leaks**: you cannot accidentally start a coroutine that runs forever after its caller returned. - **No lost errors**: an exception in a background task isn't swallowed; it propagates to the parent. - **One cancel point**: cancelling a scope (e.g. a `viewModelScope` when a screen closes) cleanly cancels all work it started. ## `coroutineScope` vs `GlobalScope` - `coroutineScope { }` / `withContext` create **structured** child scopes — preferred. - `GlobalScope.launch { }` is **unstructured** — it has no parent, isn't awaited, and isn't cancelled with anything. It's discouraged precisely because it breaks structured concurrency. ## APIs/keywords to name `CoroutineScope`, `coroutineScope { }`, `supervisorScope { }`, `Job`, `SupervisorJob`, `launch`, `async`, `Job.cancel()`, `Job.join()`, `CoroutineContext`, `GlobalScope` (anti-pattern).
- What is the difference between coroutineScope and supervisorScope?In coroutineScope, a child's failure cancels the parent and all siblings. In supervisorScope (a SupervisorJob), children fail independently — one child's failure doesn't cancel its siblings.
- Why is GlobalScope.launch discouraged?It creates an unstructured coroutine with no parent: it isn't awaited, isn't cancelled with any scope, and its failures aren't propagated — so it can leak and hide errors.
Like a manager who can't go home until every team member's task is done — and if one task blows up, the manager calls off the rest rather than letting them run blind.
saying these in an interview costs you the question
- Thinks a coroutineScope returns before its children finish
- Unaware that cancelling a scope cancels all child coroutines
- Reaches for GlobalScope as the normal way to launch work
- Doesn't know child failure propagates to parent/siblings by default
- Confuses Job (lifecycle/cancellation) with Dispatcher (thread)