Structured concurrency (StructuredTaskScope, JEP 453) treats a group of concurrent subtasks as a single unit of work with a defined lifetime: tasks forked in a scope must all complete before the scope exits, and errors and cancellation propagate as a unit. It brings the discipline of structured programming — clear entry and exit — to concurrency.
The problem it fixes
With a raw ExecutorService you can submit subtasks, then forget to handle one's failure, leak a thread when the parent returns early, or keep work running after the result is no longer needed. There is no enforced parent/child relationship, so cancellation and error handling are ad hoc and easy to get wrong. Structured concurrency makes that relationship explicit and scoped.
How it works
You open a scope in a try-with-resources block, fork() subtasks (each runs in its own virtual thread), then join() to wait for the group. Two ready-made policies:
ShutdownOnFailure— like an "all must succeed": if any subtask throws, the others are cancelled andjoinsurfaces the failure. Use for fan-out where you need every result.ShutdownOnSuccess— like a race: the first successful result cancels the rest. Use for redundant calls where any answer wins.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<User> user = scope.fork(() -> userService.fetch(id));
Subtask<Order> order = scope.fork(() -> orderService.fetch(id));
scope.join(); // wait for both
scope.throwIfFailed(); // propagate first failure, cancel siblings
return new Profile(user.get(), order.get());
} // scope closes: guaranteed no subtask is still running
Why it pairs with virtual threads
Each fork spins up a virtual thread, so forking dozens of subtasks per request is cheap. Cancellation is cooperative via interruption: when the scope shuts down, it interrupts the unfinished subtasks, which unmount and unwind. The result is fan-out/fan-in code that reads top-to-bottom yet handles partial failure and timeouts correctly.