We had to change the dispatcher (we used Dispatchers.IO)
We had to wrap blocking calls in runInterruptible
We had to pass MDCContext() to runBlocking
StructuredTaskScope
No libraries added, just JDK 25
We had to remember to call join
We had to build some helpers/wrappers to make it smooth: taskScope instead of StructuredTaskScope.open<Any>.use
We had to manually propagate MDC in our wrapper or to explore ScopedValue for this use case
When to use CoroutineScope and why?
Coroutines are, just like Kotlin, very flexible. If our use case involved manual cancellation of subtasks or some more complex concurrency patterns, coroutines and the ecosystem can probably handle it.
If our entire system is non-blocking, using a reactive framework and libraries, then coroutines are the right tool. They will make the code much more readable and less error prone.
When to NOT use CoroutineScope and why?
Coroutines are invasive. The suspend keyword will spread through the codebase like a virus. If the entire system is blocking, introducing coroutines may be disruptive and may take extra care to cover all the edge cases.
If everything is already blocking it's better to make blocking less expensive (by using virtual threads), than to replace it with non-blocking.
If stack traces and debugging are very important
When to use StructuredTaskScope
If we use Java, there is no alternative.
If everything is already blocking.
If we don't mind that it's a preview API.
If we are tired from untangling reactive/non-blocking stack traces
If we want debugging to be easy
When NOT to use StructuredTaskScope
When preview features are too scary for us
When we already have a codebase using reactive/non-blocking code
Conclusion
Both are great, one is old, the other is new, and just like Kotlin and Java, they will continue to learn from each other as they develop further.