Definition
- Structured concurrency – Treats a group of concurrent subtasks as a single unit of work with a bounded lifetime, enforcing that all subtasks complete before the parent returns.
StructuredTaskScope– The API that implements structured concurrency: you open a scope,forksubtasks (each on a virtual thread),jointo wait, then read results.- ShutdownOnFailure – Policy that cancels remaining subtasks as soon as one fails, enabling fail-fast behavior.
- ShutdownOnSuccess – Policy that cancels remaining subtasks as soon as one succeeds, enabling race-for-first-result behavior.
- Orphan elimination – The scope's
close()guarantees every forked subtask has completed or been cancelled before the parent returns.
Imagine a loan officer at National Bank of India who, to approve a loan, must get three checks done at once: a credit-bureau score, an income verification, and a fraud check. Structured concurrency is a strict rule that says the officer cannot leave the desk until all three helpers have returned, and if any one reports a hard failure, the officer immediately calls off the other two instead of wasting their time. If instead the officer only needs the fastest of several equivalent data providers, the moment one answers, the others are told to stop. Nobody is left working on a decided request, and nobody is forgotten.
Understanding the Concepts
What problem does structured concurrency solve?
Unstructured concurrency, like submitting tasks to an executor and holding futures, has no enforced relationship between the parent and its subtasks. If the parent returns early or throws, the subtasks keep running as orphans, consuming resources and logging confusing errors. Cancellation is manual and easy to get wrong.
package com.bank.service;
import java.util.concurrent.*;
import java.math.BigDecimal;
public class UnstructuredProblem {
private final ExecutorService pool =
Executors.newFixedThreadPool(10);
// PROBLEM: if creditCheck fails, fraudCheck keeps running
// as an orphan, wasting resources
public boolean approveTransfer(String userId) throws Exception {
Future<Boolean> creditFuture =
pool.submit(() -> checkCredit(userId));
Future<Boolean> fraudFuture =
pool.submit(() -> checkFraud(userId));
try {
boolean credit = creditFuture.get(); // blocks
boolean fraud = fraudFuture.get(); // blocks
return credit && fraud;
} catch (ExecutionException e) {
// Must manually cancel the other future!
creditFuture.cancel(true);
fraudFuture.cancel(true);
throw e;
}
}
private boolean checkCredit(String userId) {
return true;
}
private boolean checkFraud(String userId) {
return true;
}
}
How does StructuredTaskScope work?
You open a scope in try-with-resources, fork each subtask, call join to wait, and read results. The scope's close guarantees every subtask has finished before the parent returns.
package com.bank.service;
import java.util.concurrent.StructuredTaskScope;
import java.math.BigDecimal;
public class StructuredTransferService {
public boolean approveTransfer(String userId) throws Exception {
try (var scope = new StructuredTaskScope
.ShutdownOnFailure()) {
// Fork subtasks: each runs on a virtual thread
var creditTask = scope.fork(
() -> checkCredit(userId));
var fraudTask = scope.fork(
() -> checkFraud(userId));
var incomeTask = scope.fork(
() -> verifyIncome(userId));
// Wait for all to complete or first failure
scope.join();
scope.throwIfFailed();
// All succeeded: read results
boolean credit = creditTask.get();
boolean fraud = fraudTask.get();
boolean income = incomeTask.get();
return credit && fraud && income;
}
// close() guarantees all subtasks are done
// No orphans possible
}
private boolean checkCredit(String userId) {
System.out.println("Credit check for " + userId);
return true;
}
private boolean checkFraud(String userId) {
System.out.println("Fraud check for " + userId);
return true;
}
private boolean verifyIncome(String userId) {
System.out.println("Income verify for " + userId);
return true;
}
}
How does ShutdownOnFailure work?
With a ShutdownOnFailure policy, the first subtask that throws triggers cancellation of all other still-running subtasks. After join, throwIfFailed rethrows the first exception. This is the pattern when several independent checks must all pass.
package com.bank.service;
import java.util.concurrent.StructuredTaskScope;
public class FailFastDemo {
public boolean approveTransfer(String userId) throws Exception {
try (var scope = new StructuredTaskScope
.ShutdownOnFailure()) {
var creditTask = scope.fork(
() -> checkCredit(userId));
var fraudTask = scope.fork(
() -> checkFraud(userId));
scope.join();
// If fraud check threw, credit check was cancelled
scope.throwIfFailed();
return creditTask.get() && fraudTask.get();
}
}
private boolean checkCredit(String userId) {
try { Thread.sleep(2000); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return true;
}
private boolean checkFraud(String userId) {
// Fails fast: other tasks get cancelled
throw new RuntimeException("Fraud detected for " + userId);
}
}
How does ShutdownOnSuccess work?
With ShutdownOnSuccess, the first subtask that succeeds triggers cancellation of the rest. This is the pattern for racing several equivalent sources and taking the fastest.
package com.bank.service;
import java.util.concurrent.StructuredTaskScope;
import java.math.BigDecimal;
public class RaceForRateService {
// Race multiple providers for the best exchange rate
public BigDecimal getExchangeRate(String currency)
throws Exception {
try (var scope = new StructuredTaskScope
.ShutdownOnSuccess<BigDecimal>()) {
scope.fork(() -> fetchFromProviderA(currency));
scope.fork(() -> fetchFromProviderB(currency));
scope.fork(() -> fetchFromProviderC(currency));
scope.join();
// First successful result wins;
// other providers cancelled
return scope.result();
}
}
private BigDecimal fetchFromProviderA(String currency) {
try { Thread.sleep(100); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return new BigDecimal("83.50");
}
private BigDecimal fetchFromProviderB(String currency) {
try { Thread.sleep(200); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return new BigDecimal("83.52");
}
private BigDecimal fetchFromProviderC(String currency) {
try { Thread.sleep(300); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return new BigDecimal("83.48");
}
}
How is structured concurrency different from executor-based concurrency? (Comparison)
| Feature | StructuredTaskScope | ExecutorService + Future |
|---|---|---|
| Orphan prevention | Guaranteed by close() | Manual cancel on every path |
| Cancellation | Automatic via policy | Manual future.cancel() |
| Fail-fast | ShutdownOnFailure | Custom logic per future |
| Race pattern | ShutdownOnSuccess | Custom CompletableFuture |
| Thread type | Virtual threads (cheap) | Platform threads (heavy) |
| Scoped values | Inherited cheaply | InheritableThreadLocal (expensive) |
| Deadline support | join(Duration) | Custom timeout logic |
| Code clarity | Linear, structured | Scattered, callback-heavy |
How does structured concurrency compose with deadlines?
You can join with a timeout so the whole group is bounded in time. If the deadline expires, remaining subtasks are cancelled.
import java.time.Duration;
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var creditTask = scope.fork(() -> checkCredit("Rajesh"));
var fraudTask = scope.fork(() -> checkFraud("Rajesh"));
// Entire group must complete within 5 seconds
scope.joinUntil(java.time.Instant.now().plusSeconds(5));
scope.throwIfFailed();
return creditTask.get() && fraudTask.get();
}
How does it compose with scoped values?
Child tasks forked from a StructuredTaskScope transparently inherit scoped-value bindings cheaply, giving context propagation for free without copying a per-thread map.
import java.lang.ScopedValue;
static final ScopedValue<String> CURRENT_USER =
ScopedValue.newInstance();
ScopedValue.where(CURRENT_USER, "Rajesh").run(() -> {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// Both children inherit CURRENT_USER = "Rajesh"
scope.fork(() -> {
System.out.println(CURRENT_USER.get()); // Rajesh
return checkCredit(CURRENT_USER.get());
});
scope.fork(() -> {
System.out.println(CURRENT_USER.get()); // Rajesh
return checkFraud(CURRENT_USER.get());
});
scope.join();
scope.throwIfFailed();
}
});
What should a senior engineer know?
Structured concurrency makes concurrent code read like ordinary structured code with clear error handling instead of a tangle of futures. The single guarantee that close prevents orphans is the key design insight. The newest API iterations fold the policy into how you open the scope and provide a joiner abstraction, but the core principles of fork into a bounded scope, join, and guaranteed shutdown are stable.
Visual Overview
┌───────────────────────────────────────────────────┐ │ ShutdownOnFailure: All Must Pass │ ├───────────────────────────────────────────────────┤ │ │ │ StructuredTaskScope.ShutdownOnFailure │ │ │ │ │ ├── fork(creditCheck) ──→ SUCCESS │ │ ├── fork(fraudCheck) ──→ FAILED! │ │ └── fork(incomeCheck) ──→ CANCELLED │ │ │ │ fraudCheck threw → incomeCheck interrupted │ │ join() returns → throwIfFailed() rethrows │ │ close() guarantees all subtasks done │ └───────────────────────────────────────────────────┘ ┌───────────────────────────────────────────────────┐ │ ShutdownOnSuccess: First Wins │ ├───────────────────────────────────────────────────┤ │ │ │ StructuredTaskScope.ShutdownOnSuccess │ │ │ │ │ ├── fork(providerA) ──→ SUCCESS (fastest!) │ │ ├── fork(providerB) ──→ CANCELLED │ │ └── fork(providerC) ──→ CANCELLED │ │ │ │ providerA returned first → others interrupted │ │ join() returns → result() gives providerA answer │ │ close() guarantees all subtasks done │ └───────────────────────────────────────────────────┘
Key Takeaway
Structured concurrency treats a group of concurrent subtasks as a single unit of work with a bounded lifetime, enforcing that all subtasks complete before the parent returns. StructuredTaskScope implements this: you open a scope in try-with-resources, fork each subtask on its own virtual thread, call join to wait, and read results. The scope's close guarantees that every forked subtask has either completed or been cancelled before the parent operation returns. That single guarantee makes orphaned tasks impossible.
This fixes the real problem with unstructured concurrency. When you submit tasks to an executor and hold futures, there is no enforced relationship between the parent and its subtasks. If the parent throws, the subtasks keep running as orphans, and cancellation is manual and error-prone.
The policy decides how the group reacts. ShutdownOnFailure means the first failure cancels all others for fail-fast behavior: the pattern when several checks must all pass. ShutdownOnSuccess means the first success cancels the rest for racing several equivalent sources. In both cases cancellation is cooperative through interruption, and virtual threads unwind promptly.
In banking work at National Bank of India, transfer approval needed a credit check, income verification, and fraud check to all pass. Running them in a ShutdownOnFailure scope meant that when the fraud check failed, the other two were cancelled automatically with nothing left running in the background. For currency rates, racing several providers in a ShutdownOnSuccess scope took the fastest answer while the slower ones were cancelled.
Remember one sentence: StructuredTaskScope binds concurrent subtasks to a bounded scope with automatic cancellation on failure or success, eliminating orphaned tasks and replacing the manual future-cancellation tangle of unstructured concurrency.
---