Definition
- Structured Concurrency – a model where concurrent subtasks are scoped to a code block, ensuring all are completed or cancelled before the block exits.
- StructuredTaskScope – the API class (preview, JEP 453) that implements this model using try-with-resources.
- ShutdownOnFailure – a policy that cancels all remaining subtasks when any one throws an exception, propagating the first error.
- ShutdownOnSuccess – a policy that returns the first successful result and cancels the remaining subtasks.
- Problem solved – raw
ExecutorService.submit()lets tasks outlive their parent, leaks cancellation, and scatters error handling across multipleFuture.get()calls.
A bank transfer requires three checks: balance verification, fraud screening, and compliance review. With raw ExecutorService, if the fraud check fails, the balance and compliance tasks keep running wastefully. With StructuredTaskScope and ShutdownOnFailure, the moment any check fails, all other checks are cancelled immediately. The transfer is one unit: all succeed or all cancel.
Understanding the Concepts
What is Structured Concurrency?
Structured Concurrency (JEP 453, preview in Java 21-25) treats a group of concurrent subtasks as a single unit of work with a clear scope. You fork subtasks inside a try-with-resources block, then call join() to wait for all of them. The scope guarantees that when the block exits, all subtasks are either complete or cancelled. No threads leak.
Why does unstructured concurrency cause problems?
With raw ExecutorService.submit(), three problems arise.
First, orphaned tasks: the parent method returns but submitted tasks keep running in the background, consuming resources. Second, lost cancellation: there is no built-in way to cancel all related tasks when one fails. Third, scattered error handling: each Future.get() must be wrapped in its own try-catch, and correlating errors across subtasks is manual.
// WRONG: unstructured concurrency with raw ExecutorService
ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
Future<BigDecimal> balanceFuture = pool.submit(() -> checkBalance("SAV2001"));
Future<Boolean> fraudFuture = pool.submit(() -> checkFraud("SAV2001"));
// If fraudFuture fails, balanceFuture keeps running!
// If this method throws, both tasks are orphaned!
How does StructuredTaskScope fix this?
package com.bank.service;
import java.math.BigDecimal;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;
public class TransferService {
public boolean processTransfer(String accountId, BigDecimal amount) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<BigDecimal> balance = scope.fork(() -> checkBalance(accountId));
Subtask<Boolean> fraud = scope.fork(() -> checkFraud(accountId));
Subtask<Boolean> compliance = scope.fork(() -> checkCompliance(accountId));
scope.join(); // wait for all subtasks
scope.throwIfFailed(); // propagate first exception
// All three succeeded
return balance.get().compareTo(amount) >= 0
&& fraud.get()
&& compliance.get();
}
// Scope closed: all subtasks guaranteed complete or cancelled
}
}
Scenario 1: All succeed → transfer proceeds Scenario 2: Fraud check fails → balance and compliance cancelled immediately Scenario 3: Balance check throws → fraud and compliance cancelled immediately
What is ShutdownOnSuccess?
ShutdownOnSuccess returns the first successful result and cancels the remaining subtasks. Useful for redundant requests to multiple servers or fallback chains.
public BigDecimal getExchangeRate(String currency) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<BigDecimal>()) {
scope.fork(() -> queryPrimaryProvider(currency));
scope.fork(() -> querySecondaryProvider(currency));
scope.fork(() -> queryCachedRate(currency));
scope.join();
return scope.result(); // first successful result, others cancelled
}
}
How is this different from CompletableFuture?
| Aspect | StructuredTaskScope | CompletableFuture |
|---|---|---|
| Scope | Bounded by try-with-resources | Unbounded, can outlive parent |
| Cancellation | Automatic on failure/success | Manual, easy to forget |
| Error handling | Centralized (throwIfFailed) | Scattered across thenCompose chains |
| Thread model | Works with virtual threads | Works with any executor |
| Readability | Sequential-looking code | Callback chains |
| Status | Preview (Java 21-25) | Stable since Java 8 |
What are the rules for using StructuredTaskScope?
- Always use try-with-resources to ensure the scope is closed.
- Call
join()before accessing subtask results. - Call
throwIfFailed()after join to propagate exceptions. - Do not share Subtask objects outside the scope.
- Fork only inside the scope that owns the subtask.
What are common mistakes?
// WRONG: accessing result before join
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<BigDecimal> bal = scope.fork(() -> checkBalance("SAV2001"));
BigDecimal result = bal.get(); // IllegalStateException: not joined yet!
}
// WRONG: not calling throwIfFailed
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<BigDecimal> bal = scope.fork(() -> checkBalance("SAV2001"));
scope.join();
// Missing scope.throwIfFailed()!
BigDecimal result = bal.get(); // May get result from a FAILED subtask!
}
What should a senior engineer know?
- Structured concurrency makes concurrent code read like sequential code with reliable cancellation and error propagation.
- It pairs naturally with virtual threads: each forked subtask runs on its own virtual thread.
- ScopedValues (also preview) pass context (request ID, user principal) to forked subtasks without ThreadLocal.
- The API may change before finalization. Do not use in public library APIs until it is final.
What are common interview follow-up questions?
- Can you nest StructuredTaskScopes? (Yes. An inner scope's subtasks are cancelled when the inner scope exits.)
- Does StructuredTaskScope replace CompletableFuture? (Not entirely. CompletableFuture is stable and supports more composition patterns. StructuredTaskScope is for fan-out/fan-in with cancellation.)
- What happens if the parent thread is interrupted? (The scope shuts down and cancels all subtasks.)
Visual Overview
Structured Concurrency Flow:
┌───────────────────────────────────────┐
│ try (scope = ShutdownOnFailure()) │
│ │
│ fork ──▶ Subtask 1 (balance) │
│ fork ──▶ Subtask 2 (fraud) │
│ fork ──▶ Subtask 3 (compliance) │
│ │
│ scope.join() ◀── wait for all │
│ throwIfFailed() ◀── propagate err │
│ │
│ Use results safely │
│ │
└───────────────────────────────────────┘
│ scope.close() guarantees:
▼ all subtasks done or cancelled
On failure:
Subtask 2 throws ──▶ Subtask 1 cancelled
──▶ Subtask 3 cancelled
──▶ throwIfFailed() rethrows
Key Takeaway
Structured Concurrency treats concurrent subtasks as one unit using StructuredTaskScope. ShutdownOnFailure cancels all siblings when any subtask fails, giving all-or-nothing semantics. ShutdownOnSuccess returns the first successful result and cancels the rest, useful for redundant requests. The try-with-resources scope guarantees that all subtasks are complete or cancelled when the block exits.
This fixes three problems of raw ExecutorService: orphaned tasks that outlive their parent, lost cancellation with no way to stop related tasks, and scattered error handling across multiple Future.get() calls. With structured concurrency, you fork subtasks, join, and throwIfFailed in three clean lines.
In a banking transfer service, a transfer requires balance verification, fraud screening, and compliance review. With ShutdownOnFailure, if the fraud check fails, the balance and compliance tasks are cancelled immediately. No wasted work, no orphaned threads, no manual cancellation code. The API is preview through JDK 25 and not yet stable for production contracts, but it represents the future direction of Java concurrency.
Remember one sentence: StructuredTaskScope forks subtasks as one unit and automatically cancels siblings on failure, fixing the orphaned-task and lost-cancellation problems of raw ExecutorService.
---