Structured Concurrency
Structured Concurrency
Table of contents
Structured Concurrency
Don't we have enough articles on Structured Concurrency already? ๐ค๐คฃ I have an ambitious goal for my article to become the definitive guide on Structured Concurrency! ๐ช My plan is to explain the concepts clearly and concisely (Kotlin-style). Sample code snippets will illustrate the concepts. Additional material and resources are optional but can be used to gain a deeper understanding of its motivation, potential upcoming changes, and more. If you have any questions or have suggestions to improve the content, please let me know via a comment.
What is Structured Concurrency?
Structured concurrency is the Kotlin coroutines' mechanism that keeps track of a hierarchy of coroutines which works as a unit (I mean: it works as an entity, please don't confuse it with the Unit keyword) and is used to avoid resource leaks, avoid running unnecessary processes, properly await completion, and proper error handling.
What is a hierarchy of coroutines?
A hierarchy of coroutines consists of the original coroutine (launched in a given scope), its children, their children, etc.
For example:
Why is Structured Concurrency important?
๐ฏ Completion
A parent cannot complete until all its children have completed. The completion guarantee is what makes Structured Concurrency structured. It transforms what might be a messy graph of concurrent tasks into a well-defined tree with a clear root, clear ownership, and a clear end. And that makes the code much easier to understand and reason.
โ Without Structured Concurrency
โ With Structured Concurrency
๐ Cancellation
Structured Concurrency ensures that when a scope is canceled, all of its child coroutines are automatically canceled (NOTE: sibling coroutines are not canceled). This prevents orphan coroutines from running, consuming resources, and potentially causing memory leaks. If we were running on a cell phone, we would be conserving precious battery life. Why bother running unnecessary processes if their result would have been consumed by a coroutine that has been canceled?
โ Without Structured Concurrency
โ With Structured Concurrency
โน๏ธ NOTE: Structured Concurrency relies on code being Cooperative to Cancellation to enforce the Cancellation guarantee. Suspend functions from
kotlinx.coroutinesare cancellable. The code you write should also be cancellable. This can be done by either checkingjob.isActiveorensureActive()or by allowing other work to happen by callingyield().
๐ฅ Exceptions
If a child coroutine completes with an exception, Structured Concurrency ensures that the exception is propagated to the parent which will then cancel all sibling coroutines, and it will then re-throw the exception to its caller. Let's say that your process starts many coroutines, and one of them throws an exception -> all the other coroutines will be canceled automatically.
This is important because:
- It ensures that exceptions are not silently ignored
- Other coroutines in the tree are canceled, so no resources are wasted
- Everything succeeds or nothing succeeds. Similar to a transaction in a database.
โ Without Structured Concurrency
The exception is not propagated to the caller. It either crashes silently or requires a separate CoroutineExceptionHandler to be caught. Also, other coroutines keep running. With Structured Concurrency, they would have been canceled.
โ With Structured Concurrency
๐จโ๐ฉโ๐งโ๐ฆ Context inheritance
Children inherit their parents' context.
โ Without Structured Concurrency
The child uses its own hardcoded scope, so the parent's Dispatcher and CoroutineName (and any other context elements like MDCContext for logging) are silently lost.
โ With Structured Concurrency
Children automatically inherit their parents' context. Dispatcher, name, and any custom context elements, such as tracing or logging MDC, are carried through without any extra wiring.
๐ง Conceptual understanding
Last but not least: Structured Concurrency makes it easy to reason about a hierarchy where the lifetime of a concurrent operation is limited by the scope in which it is launched.
How to use Structured Concurrency?
The nice thing about using Structured Concurrency in Kotlin is that we don't need to do anything special. It is the standard behavior now. Originally, this was not the case. Scary times ๐ป. Read more about this later.
โน๏ธ NOTE: Structured Concurrency creates a hierarchy that represents the parent-child relationships established when launching the coroutines within a scope.
When not to use Structured Concurrency?
Most of the time, you should rely on Structured Concurrency.
An exception to this rule is when you either don't want to (and hopefully there is a good reason for it):
- await the completion of all coroutines in a hierarchy and/or
- cancel all coroutines in the hierarchy when one is canceled and/or
- inherit the
contextfrom the parent coroutine
How to bypass certain elements of Structured Concurrency
Bypass automatic cancellation on failure
supervisorScope is very similar to coroutineScope, but a failure in one child doesn't automatically cancel the others.
This way, exceptions from its children are ignored (they only call the coroutine exception handler, so by default, that is print stacktrace).
Beware that supervisorScope only ignores exceptions from its children. If an exception occurs within supervisorScope itself, it breaks this coroutine builder and the exception propagates to its parent.
If an exception occurs in a child of a child, it propagates to the parent of this child, destroys it, and only then gets ignored.
supervisorScope is often used when we need to start multiple independent processes, and we don't want an exception in one of them to cancel the others.
Non-cancellable context
When a coroutine is canceled, we know that, thanks to Structured Concurrency, all the coroutines in its hierarchy are also canceled.
Imagine having a coroutine in this hierarchy that is used for doing some cleanup. But if the cleanup requires launching other coroutines, that wouldn't normally work, because the coroutine is in state=Cancelling. To bypass that, and as a workaround, we need to launch that clean up coroutine withContext(NonCancellable).
Jobs and Structured Concurrency
๐ฉ BEWARE: When you
launcha coroutine, you can optionally pass a context (among other things). If that context includes a Job, that will break Structured Concurrency, and it's very likely not what you were trying to do.
The same thing applies to launching a coroutine with async or withContext.
Similarly, we are not supposed to override the Job in a coroutine starter (launch, async, withContext) context:
To make things more confusing, Job is the only coroutine context that is not inherited by a coroutine from another coroutine. ๐คฆโโ๏ธ
๐งโโ๏ธ Coroutines mastery course
As part of the amazing Coroutines Mastery class taught by Marcin Moskaลa, we got exclusive access to Q&A with Roman Elizarov, the original creator of Kotlin Coroutines, former Kotlin Team Lead at JetBrains and also with Vsevolod Tolstopyatov, Kotlin Team Lead at JetBrains, responsible for the roadmap and future of coroutines. Please consider joining the next edition of Coroutines Mastery, starting in November 2026.
๐๏ธ Motivation for Structured Concurrency
Roman gave us background on the motivation for Structured Concurrency and some of its history.
There seems to be a myth that says that Roman was inspired by this article to come up with Structured Concurrency. He dispelled that rumor by saying that the article only helped him to come up with the term "Structured Concurrency" ๐คฃ. In the article I listed under [Resources], he recommends reading it, so it may have inspired him beyond just the term.
He also told us C# and Go were great inspirations, but they lacked a mechanism like Structured Concurrency.
In very early workshops, developers were shown the proper way to create coroutines, and it was noticed that even though the developers were instructed to pass the parent's Job when launching a coroutine, many times the developers forgot. This caused problems with cancellations and with exceptions. That's when the team decided to enforce Structured Concurrency by automatically building the hierarchy, since that is what we want 99% of the time.
Instead of asking developers to code for the default (and desired) behavior, we now only need to write special code for edge cases. That is likely to change. See the next section ๐
๐ What's next for Structured Concurrency?
Vsevolod talked about the future of coroutines and hinted at future improvements to Structured Concurrency:
- Instead of simply keeping track of the hierarchy as in the parent-child relationship of coroutines, there are plans to additionally keep track of what that relationship means. This can include whether the child wants to be canceled if the parent is canceled, or whether an exception in the child should also be thrown in the parent or not. This change is expected in experimental builds soon.
- Once this change is available, we won't have to use:
- โ
supervisorScopeorCoroutineScope(SupervisorJob())because we will be able to specify during thelaunchthat we want to handle our own exceptions - โ the (ugly) workaround of launching a cleanup coroutine
withContext(NonCancellable)because we will be able to specify during thelaunchthat we want this coroutine to be created even if the parent has been canceled - โ the trick of injecting a scope when you want to
launchcoroutines, but you don't want to tie their lifetime to a specific scope, because we will be able to specify during thelaunchthat we don't want our coroutine to be tied to a specific scope - There are no plans to modify the behavior of waiting for all the children to complete. That behavior is at the core of Structured Concurrency, and getting rid of that would mean the end of Structured Concurrency ๐
- Ultimately, the goal is to allow us developers to express our intent clearly and not get lost with special code that works around odd situations and edge cases.
- Support for Rich Errors. Vsevolod said that it was planned, but didn't know how or when this would happen.
- Gradual introduction of context parameters. This change would unblock quite a few use cases, such as the proper nesting of coroutine scopes with the respective names and also suspense properties (getters and setters). This change will likely take longer to be released.
๐ค IntelliJ IDEA / Android Studio plugin
I highly recommend using Santiago Mattiauda's Structured Coroutines plugin as it will look for coroutine antipatterns in your code and offer fixes to follow best practices.
Clicking on the See guide button takes you here: 1.3 SCOPE_003 โ Breaking Structured Concurrency
โ๏ธ Structured concurrency compared to Java and other programming languages
Java (and many other programming languages) used unstructured concurrency via Thread or ExecutorService.
In that model:
- Threads are not bound to a specific scope. They can easily outlive the function that started them. There is no automatic cancellation. You must manually keep track of
Futureobjects or thread references. - Error handling is manual and error-prone. If a background thread fails, the parent often is not aware.
With the introduction of Virtual Threads (Project Loom) and the Structured Concurrency API (JEP 453), Java is moving toward a model very similar to Kotlin's, using StructuredTaskScope to ensure that subtasks are completed before the scope closes. Java always seems to play catch up with Kotlin. ๐
Sample code in Java:
// Java
Invoice createInvoice(int orderId, int customerId, String language)
throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<Order> orderSubtask =
scope.fork(() -> orderService.getOrder(orderId));
Subtask<Customer> customerSubtask =
scope.fork(() -> customerService.getCustomer(customerId));
Subtask<InvoiceTemplate> invoiceTemplateSubtask =
scope.fork(() -> invoiceTemplateService.getTemplate(language));
scope.join();
Order order = orderSubtask.get();
Customer customer = customerSubtask.get();
InvoiceTemplate template = invoiceTemplateSubtask.get();
return Invoice.generate(order, customer, template);
}
}๐ Resources
Who am I?
Hi! My name is Pato Moschcovich. I โค๏ธ Kotlin! I've been using it for over 4 years, and I enjoy it so much. My favorite thing is how expressive the language is and how simple it is to understand code and to write it. I am a Backend Software Engineer at a great company called Inductive Automation Let's connect on LinkedIn
