Structured Concurrency — Direct Way
Structured Concurrency is one of the most exciting additions coming to Java, currently in its fifth preview as part of JEP 505 in Java 25.
This feature aims to simplify how we write, manage, and reason about parallel execution, especially when multiple tasks must run together, follow a defined order, or coordinate success and failure rules.
In traditional multithreading, threads often run independently, and handling errors or cancellations across them can be messy and error-prone. Structured Concurrency changes that by treating multiple tasks as part of a single structured unit of work, like a block of code, where child tasks are automatically managed by their parent.
Let’s explore how this works in practice, and why this feature is such an important step toward making concurrent programming in Java safer, cleaner, and easier to reason about.
Example: Using StructuredTaskScope
In this example, we run two external calls in parallel:
fetch user → succeeds
fetch account → fails
With the default scope (StructuredTaskScope.open()), the rule is:
If any subtask fails, cancel the remaining subtasks.
It uses the Joiner.awaitAllSuccessfulOrThrow() by default.
void handleAwaitAllSuccessfulOrThrow() throws Exception {
try (var scope = StructuredTaskScope.open()) {
StructuredTaskScope.Subtask<String> users =
scope.fork(() -> executeExternalCall(Behavior.run(”fetch user”, 500)));
StructuredTaskScope.Subtask<String> account =
scope.fork(() -> executeExternalCall(Behavior.fail(”fetch account”, 500)));
scope.join(); // Wait for tasks
formatResults(users, account);
}
}
📌 Helpful utility methods and examples can be found here: GitHub project
Output
19:43:31.726 VThread[#20]: Initialize - fetch user
19:43:31.727 VThread[#22]: Initialize - fetch account
19:43:31.727 VThread[#22]: Ending with Error - fetch accountAs soon as the account task fails, the scope cancels the user task.
No extra waiting. No dangling work.
This is the power of Structured Concurrency.
Without Structured Concurrency
Let’s compare this with the same logic implemented using a plain ExecutorService:
void withoutStructuredTaskScope() throws Exception {
try (var executorService = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> users =
executorService.submit(() -> executeExternalCall(Behavior.run(”fetch user”, 500)));
Future<String> account =
executorService.submit(() -> executeExternalCall(Behavior.fail(”fetch account”, 500)));
if (users.isDone() && account.isDone()) {
System.out.println(users.get());
System.out.println(account.get());
}
}
}
Output
19:51:36.278 VThread[#20]: Initialize - fetch user
19:51:36.279 VThread[#22]: Initialize - fetch account
19:51:36.279 VThread[#22]: Ending with Error - fetch account
19:51:36.786 VThread[#20]: Ending - fetch user
Here’s the key problem: Even though the fetch account fails immediately, the executor lets the user task keep running.
There is no built-in coordination, no cancellation, no shared failure policy.
This is precisely what Structured Concurrency fixes.
🎯 Final Thoughts
Structured Concurrency moves Java closer to a world where:
Concurrency is predictable
Code is easier to read
Failures are handled consistently
Tasks behave like structured blocks, not loose threads
If your application deals with parallel tasks, calling services, processing data, aggregating results, Structured Concurrency can make your code cleaner, safer, and far easier to maintain.
If you want to dive deeper into this topic, check out the excellent talk on Concurrency in Action by Nicolai Parlog at Devoxx 2025. Also, check out the amazing book Modern Concurrency in Java by A N M Bazlur Rahman, which explains these concepts in depth with practical examples.
If you want to keep learning about Structured Concurrency, subscribe for free, the next article will cover Joiner.


