Introduction to Structured Concurrency: CoroutineScope & CoroutineContext
Part 1: Coroutine Scopes, Contexts, Job Hierarchy, and more
Foreword: This article is made possible thanks to my Patreon supporters, who specifically requested a deeper dive into the topic of coroutine cancellations and exceptions. This piece serves as a necessary prelude to understanding how to build automated control mechanisms using Kotlin coroutines.
This is a follow-up to my Droidcon NYC 2025 talk on Kotlin Coroutine Mechanisms — if you missed it, you can view the slides here. If you find this article useful, consider checking out my Patreon for more.
Topic Navigation
Many of you have likely been using Kotlin coroutines for some time. Perhaps you could say you’ve grown comfortable with daily use. But for most developers, the practices of structured concurrency feel elusive at best.
Fortunately (or unfortunately), I’ve spent an unreasonable amount of time researching Kotlin coroutines in the last 6 months. After many late nights and too much caffeine, I’m here to share my knowledge on the cancellation and exception-handling mechanisms behind structured concurrency. However, before we can fully understand the value of automated mechanisms, it is essential to grasp what Structured Concurrency entails.
What is Structured Concurrency?
Kotlin coroutines are designed for structured concurrency, but it can only be achieved with the intentional use of Kotlin coroutines. The Structured Concurrency paradigm can be boiled down to two general rules:
- A parent coroutine always waits for its children to complete.
- The parent coroutine never “loses” a coroutine.
When you follow these rules, you have structured concurrency. Today, we start with the first rule: A parent coroutine always waits for its children to complete.
This first rule can be achieved with the mindful use of CoroutineScope and CoroutineContext.
CoroutineScope v. CoroutineContext
CoroutineScope and CoroutineContext are used together to tie lifecycles in a Job hierarchy.
Intentional use of scope and context can tie jobs together to create predictable, safe concurrency. I always love to lift the hood to check on the internals, so that is what we will do. If you look at the interface details of CoroutineScope.kt, you’ll see that it contains only one element: the context of the scope.
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}Digging a little deeper into the implementation details of CoroutineContext.kt, you’ll see that it’s created in a Composite Design Pattern. A shortened version of the implementation details is provided below:
@kotlin.SinceKotlin public interface CoroutineContext {
public abstract operator fun <E : CoroutineContext.Element> get(key: CoroutineContext.Key<E>): E?
public abstract fun <R> fold(
initial: R,
operation: (R, CoroutineContext.Element) -> R
): R
public open operator fun plus(
context: CoroutineContext
): CoroutineContext { /* compiled code */ }
public abstract fun minusKey(key: CoroutineContext.Key<*>): CoroutineContext
public interface Key<E : CoroutineContext.Element> { }
public interface Element : CoroutineContext {
public abstract val key: CoroutineContext.Key<*
public open operator fun <E : CoroutineContext.Element> get(...): E? { /* compiled code */ }
public open fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): R { /* compiled code */ }
public open fun minusKey(key: kotlin.coroutines.CoroutineContext.Key<*>): kotlin.coroutines.CoroutineContext { /* compiled code */ }
}
}The Composite Design Pattern enables the ability to able to create unique and traceable tree structures, which allows for the ability to keep track of nested scopes and where execution takes place. In short:
- A
CoroutineScopeacts as a container for CoroutineContext. It defines a group of coroutine jobs working together. - A
CoroutineContextis a set of mapped composite elements that is used to describe the coroutine behavior.
Roman Elivaroz explores this concept in depth in his article Coroutine Context and Scope, where he introduces a visual model that illustrates the relationship between scope and context. I found his explanation particularly insightful while preparing for my Droidcon NYC ’25 presentation.
Below is a diagram I rendered for that talk, inspired by his original diagram:
Together, the composite structure of a CoroutineContext enables the tracking of a coroutine’s Job hierarchy within a given scope.
Job Hierarchy vs. Scope Hierarchy
In the context of structured concurrency, a job hierarchy generally respects the tenets of structured concurrency.
When a Job is launched in another Job, they create a child-parent hierarchy automatically. These jobs exist in the same scope.
However, the same is not true for a new CoroutineScope. A new CoroutineScope(…) does not inherit the parent scope just because it launches in another scope.
Not all coroutine scopes are created equally. In fact, there are only a few options when working with scope that respect the structured concurrency paradigm:
Finally, after digging into discussions, there are a few helpful tips to use CoroutineScope and Context, as eloquently explained by Roman Elizarov his comment on https://github.com/Kotlin/kotlinx.coroutines/issues/1001#issuecomment-814261687
The rule of thumb that is we use in API:
1. If it is using
CoroutineContextthen you should NOT haveJobthere.2. If it is using
CoroutineScopethen the scope should have aJob.
Now that we understand the basics of coroutine scope and context in the scope of structured concurrency, let’s play with these ideas in a short code example.
Does this code example respect structured concurrency?
For this series, we will work with a class named Kitchen.kt, which represents a kitchen taking orders defined in a kitchenScope. Each order is a Job type, and in the code, we input an order for salad and pasta.
Take a close look: does this code example respect structured concurrency?
Running this default class gives us the following output.
Making salad | current thread: DefaultDispatcher-worker-1
Making pasta | current thread: DefaultDispatcher-worker-2
Boiling water... | current thread: DefaultDispatcher-worker-3
Cooking pasta... | current thread: DefaultDispatcher-worker-4
Salad is ready! | current thread: DefaultDispatcher-worker-4
Pasta is cooked. | current thread: DefaultDispatcher-worker-4
Water is hot! | current thread: DefaultDispatcher-worker-4
Pasta is ready! | current thread: main
All dishes are prepared! | current thread: main
Process finished with exit code 0It looks pretty close, but the answer is no.
saladOrder runs in parallel to pastaOrder. Isn’t that a problem?
Structured concurrency doesn’t mean that tasks cannot run in parallel. Rather, structured concurrency is concerned with knowing what child jobs are running and when they complete.
We’ve indicated that at the bottom of the code by asking for a join statement. This ensures that the parent waits on saladOrder for completion.
fun main() = runBlocking {
val parentJob = Job()
val kitchenScope = CoroutineScope(parentJob)
val saladOrder: Job = kitchenScope.launch {
log("Making salad ")
delay(400)
log("Salad is ready! ")
}
val pastaOrder: Deferred<String> = kitchenScope.async {
...
}
saladOrder.join() // <-- forces parent to wait
val pastaResult = pastaOrder.await()
log(pastaResult)
log("All dishes are prepared!")
}In short, it doesn’t matter the ordering of the coroutines or whether they run in parallel. It only matters that the parent knows when the child is supposed to finish.
Why does saladOrder respect structured concurrency, but the child tasks of pastaOrder don’t?
Inside the pastaOrder, the two child coroutines are launched, which inherit the current scope, but the pastaOrder coroutine does not wait on the child coroutines for completion.
That means it’s possible for pastaOrder to complete, even if something goes wrong with one of the child tasks.
But the code works out in the end… so no problems, right?
Well, not for this article. In the next article, we will start adding cancellation mechanisms to expose where behaviors can become unpredictable and hard to track.
Fixing Kitchen.kt for Structured Concurrency
To fix the issue with pastaOrder, there are several ways to address it. One way is to utilize coroutineScope {…} which is one of the main ways to inherit the parent scope.
In essence, coroutineScope {…} draws a sub-scope that forces pastaOrder to wait on the completion of the sub-scope before continuing.
The code above respects structured concurrency. We defined a root job and CoroutineScope, which is the root of all launched coroutines. Every child coroutine inside both orders is also tied to the parent job.
The pastaOrder child coroutines, boilWater and cookPasta, are wrapped in coroutineScope {…} which inherits from the parent context. This means that the parent scope is aware of the inner scope, and the two are tied together.
Additionally, coroutineScope {…} is a block-scope context, so all internal work within it must be complete before moving on. As a result:
- Child coroutines are bound to the lifecycle of the parent coroutine
- No coroutine outlives the parent scope.
And that’s it for today! You now have enough preparation to dive into the real subject of exploration — coroutine cancellations and mechanisms in part 2.
Topic Navigation
Sources Cited
- Antanas-Arvasevicius. “Structured Concurrency with Two Parent Jobs · Issue #1001 · Kotlin/Kotlinx.Coroutines.” GitHub, 20 Feb. 2019, https://github.com/Kotlin/kotlinx.coroutines/issues/1001
- Elizarov, Roman. “Coroutine Context and Scope.” Medium, 9 Mar. 2019, https://elizarov.medium.com/coroutine-context-and-scope-c8b255d59055
- Hinchman-Dominguez, Amanda. “Kotlin Coroutine Mechanisms: A Surprisingly Deep Rabbithole.” Speaker Deck, 25 June 2025, https://speakerdeck.com/amanda_hinchman/kotlin-coroutine-mechanisms-a-surprisingly-deep-rabbithole
Want more on Structured Concurrency?
I’ve written a few books and done a few talks on this topic. Check them out!
- Droidcon NYC Talk is now out on Youtube — https://www.youtube.com/watch?v=7BVriPGoJxc&ab_channel=droidcon%26fluttercon
- Programming Android with Kotlin: Achieving Structured Concurrency with Coroutine — https://www.amazon.com/Programming-Android-Kotlin-Structured-Concurrency/dp/1492063002
- [COMING OUT SOON] Droidcon Academy Master Codelabs: Mastering Kotlin Coroutines: From Basics to Advanced — https://academy.droidcon.com/course/setup-mastering-kotlin-coroutines-from-basics-to-advanced

