Sitemap
Coding Kinetics

Master Kotlin without the overwhelm. Cut through the noise. Learn your way — alone or in good company.

Introduction to Structured Concurrency: CoroutineScope & CoroutineContext

Part 1: Coroutine Scopes, Contexts, Job Hierarchy, and more

7 min readJul 28, 2025

--

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

Press enter or click to view image in full size

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:

  1. A parent coroutine always waits for its children to complete.
  2. 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 CoroutineScope acts as a container for CoroutineContext. It defines a group of coroutine jobs working together.
  • A CoroutineContext is 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:

Press enter or click to view image in full size
See the original diagram here, also found in the article article Coroutine Context and Scope

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:

Press enter or click to view image in full size
table with different ways to launch coroutines. first column is how to launch, second column is respect structured concurrency? and third column is inherited or new scope? 1. CoroutineScope(…).launch { … } No New scope 2. coroutineScope { … } Yes Inherits parent 3. scope GlobalScope.launch { … } No Global scope 4. viewModelScope.launch { … } Yes Inherits from ViewModel 5. lifecycleScope.launch { … } Yes Inherits from lifecycle object

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 CoroutineContext then you should NOT have Job there.

2. If it is using CoroutineScope then the scope should have a Job.

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 0

It looks pretty close, but the answer is no.

Press enter or click to view image in full size

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:

  1. Child coroutines are bound to the lifecycle of the parent coroutine
  2. No coroutine outlives the parent scope.
Press enter or click to view image in full size

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

Want more on Structured Concurrency?

I’ve written a few books and done a few talks on this topic. Check them out!

--

--

Coding Kinetics
Coding Kinetics

Published in Coding Kinetics

Master Kotlin without the overwhelm. Cut through the noise. Learn your way — alone or in good company.

Amanda Hinchman
Amanda Hinchman

Written by Amanda Hinchman

Kotlin GDE, Android engineer & O'Reilly book author | Support my research on Patreon: patreon.com/AmandaHinchman

Responses (1)