Structured Concurrency & Scoped Values
Java 21-25's structured concurrency treats a group of concurrent tasks as one unit of work, with scoped values as a safe replacement for thread-locals.
On this page
Spawning concurrent tasks with an executor works, but it's unstructured: if you start three subtasks and one fails, the others keep running, error handling is manual, and cancellation is fiddly. Structured concurrency (finalized around Java 25, after previews from 21) brings the discipline of structured programming to threads: a group of concurrent subtasks becomes one unit of work with a clear scope.
Concurrency with a scope
The idea: if you split a task into concurrent subtasks, they should all complete (or all be cancelled)
before the parent continues - just as a method's local variables live and die within its braces.
StructuredTaskScope enforces this:
// run two subtasks concurrently as one unit of work (Java 21-25 API)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var user = scope.fork(() -> fetchUser(id)); // subtask 1
var orders = scope.fork(() -> fetchOrders(id)); // subtask 2
scope.join(); // wait for both
scope.throwIfFailed(); // if either failed, propagate and cancel the other
return new Summary(user.get(), orders.get()); // both succeeded
} // scope closes: any still-running subtask is guaranteed cancelledThe try-with-resources block is the lifetime of the concurrent work. You can't leak a subtask past
the block, and ShutdownOnFailure gives you the sensible default: if one subtask fails, the others are
cancelled immediately rather than wasting effort. This is the opposite of the old executor model,
where a failed subtask left its siblings running blindly.
Why "structured" matters
Structured concurrency makes concurrent code reason like sequential code: clear scope, automatic
propagation of errors and cancellation, and no orphaned threads. Combined with virtual threads (each
fork runs on one), you get readable, correct, highly concurrent code without the bookkeeping executors
demand.
Scoped values: better than thread-locals
A companion feature, scoped values, replaces the error-prone ThreadLocal for passing context (a user
id, a request id) down a call chain. A scoped value is immutable and bound only for a bounded
dynamic scope, which is safe and efficient with millions of virtual threads (where thread-locals would be
costly and leak-prone):
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
ScopedValue.where(CURRENT_USER, user).run(() -> handleRequest());
// inside handleRequest and anything it calls: CURRENT_USER.get() returns user, immutablyThe parallel with structured programming
Decades ago, goto gave way to structured control flow (if/loops/functions) with clear entry and exit. Structured concurrency is the same leap for threads: a scope has one entry and one exit, subtasks can't outlive it, and errors and cancellation flow along the structure. It turns 'fire off threads and hope' into concurrency you can actually reason about.
The old executor model is like handing out three tasks to contractors and walking away - if one quits, the others keep working on a project that's already doomed, and nobody's tracking them. Structured concurrency is a project manager who splits the work, stays until every subtask finishes, and the moment one fails, calls off the rest so no effort is wasted - then reports a single result. The team's work is bracketed by the manager's presence: nothing outlives the meeting, and one clear outcome comes out. That bracketing is the 'structure.'
You fork two concurrent subtasks and only want a result if both succeed - if either fails, you want to stop the other immediately and report the error. Explain how structured concurrency handles this compared to submitting two tasks to a plain ExecutorService.
What problem does structured concurrency solve compared to submitting tasks to a plain executor?
Key takeaways
- Structured concurrency (Java 21-25) treats a group of concurrent subtasks as one unit of work with a clear scope.
- A StructuredTaskScope ties subtask lifetimes to a try-with-resources block: none can outlive it, and ShutdownOnFailure cancels siblings when one fails.
- It makes concurrent code reason like sequential code - automatic error propagation and cancellation, no orphaned threads.
- Each fork typically runs on a virtual thread, combining readability with high concurrency.
- Scoped values replace ThreadLocal for passing immutable context down a call chain - safe and efficient with millions of virtual threads.