Structured concurrency in Java
StructuredTaskScope (preview): treating related tasks as one unit.
Tasks as one unit
Structured concurrency treats a group of related subtasks as one unit of work, scoped to a code block. Subtasks can't outlive their scope, so no orphan threads leak. Status check: in Java 25, StructuredTaskScope is still a preview API, needing --enable-preview, and its design has changed between previews.
open, fork, join, get
open() creates the scope. fork(task) starts a subtask in a new virtual thread. join() waits for the subtasks according to the scope's policy. Then get() reads each result. Try-with-resources guarantees cleanup.
try (var scope = StructuredTaskScope.open()) {
var user = scope.fork(() -> findUser(id));
var cart = scope.fork(() -> findCart(id));
scope.join();
return new Page(user.get(), cart.get());
}Unstructured vs structured
var u = pool.submit(() -> fetchUser(id));
var o = pool.submit(() -> fetchOrders(id));
return new Page(u.get(), o.get());If fetchUser fails, u.get() throws, but fetchOrders keeps running in the background: a leaked task.
try (var s = StructuredTaskScope.open()) {
var u = s.fork(() -> fetchUser(id));
var o = s.fork(() -> fetchOrders(id));
s.join();
return new Page(u.get(), o.get());
}The scope owns both lifetimes: by default, one failure cancels the sibling and join() throws.
Your turn
With the default scope, fetchUser fails after 50 ms; fetchOrders would take 30 s. When does join() return?
After about 50 ms, throwingAfter 30 s, throwingAfter 30 s, returning normally
Show the answer
About 50 ms. The default policy waits for all subtasks to succeed; as soon as one fails, the scope cancels the others (interrupting their threads), and join() throws. No wasted 30 seconds.
get() before join()
Reading a subtask before the scope's owner has called join() throws IllegalStateException, even if that subtask has already finished. The rule: fork everything, join(), and only then call get().
var a = scope.fork(() -> 20);
a.get(); // IllegalStateException!
scope.join();
a.get(); // fine: 20Policies
open() with no arguments uses the default policy: all must succeed, and the first failure cancels the rest. Other policies can be plugged in, such as "first success wins" for racing two replicas. Either way, the scope, not you, tracks every thread it started.
Fan-out without leaks
Request handlers often call two to five services at once. Structured concurrency gives them clean failure semantics, no orphaned background work after a timeout or error, and thread dumps that can show the parent-child tree of tasks. Because it's preview, keep it out of code that must compile without --enable-preview.
Key takeaways
- try (var scope = StructuredTaskScope.open()) { fork…; join(); }
- By default one failed subtask cancels the others
- Subtasks can't outlive the scope — no orphan threads
- Call join() before reading any Subtask.get()
StructuredTaskScope has been incubating or in preview since Java 19, and its API was redesigned along the way. That's exactly what preview is for.
Practice questions
A handler starts fetchUser() and fetchOrders() on an ExecutorService. fetchUser() fails fast, but fetchOrders() keeps running for 30 s after the request has already failed. What does structured concurrency give you here?
- Sibling subtasks are cancelled automatically when one fails
- fetchUser() is retried automatically
- Both calls share one thread to save resources
- Exceptions are ignored so the request succeeds
Check your answer
Sibling subtasks are cancelled automatically when one fails. With the default policy, the scope cancels the remaining subtasks as soon as one fails, and join() throws.
What must replace the blank before the results can be read?
try (var scope = StructuredTaskScope.open()) {
var user = scope.fork(() -> findUser(id));
var cart = scope.fork(() -> findCart(id));
scope.___();
return new Page(user.get(), cart.get());
}- join
- close
- shutdown
- awaitTermination
Check your answer
join. join() waits for the subtasks according to the scope's policy. Reading a Subtask before join() throws IllegalStateException.