Was this article helpful?
to mark as helpful
Enjoyed this article?
Get more engineering insights delivered weekly.
Comments
to join the discussion
to mark as helpful
Get more engineering insights delivered weekly.
to join the discussion
Jagdish Salgotra
Apr 12, 2026 · 10 min read · Structured Concurrency
Orchestrate complex Java workflows with progressive results and hierarchical task management. Master StructuredTaskScope patterns for real-time progress updates, parent-child relationships, and safe dependency handling in concurrent systems.
Your article assistant
Ask me anything about this article. I'll provide answers with relevant sources.
Try asking:
Note Examples in this article use Java 21 preview
StructuredTaskScopeAPIs (JEP 453). See Part 9 for Java 25 migration mapping. Compile and run with--enable-preview.
Many operations are not just "run N tasks and wait":
Structured concurrency can support this without abandoning clear lifecycle boundaries.
Java 21 preview provides joinUntil(...), which can be used for polling-style progress loops.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
List<StructuredTaskScope.Subtask<T>> subtasks = new ArrayList<>();
for (int i = 0; i < request.getTasks().size(); i++) {
final int taskIndex = i;
Callable<T> task = request.getTasks().get(i);
subtasks.add(scope.fork(() -> {
try {
logger.debug("Starting task {}", taskIndex);
T result = task.call();
progressTracker.updateProgress(executionId, taskIndex, "completed");
request.getProgressCallback().accept(new ProgressUpdate<>(taskIndex, result, null));
logger.debug("Task {} completed successfully", taskIndex);
return result;
} catch (Exception e) {
logger.debug("Task {} failed: {}", taskIndex, e.getMessage());
progressTracker.updateProgress(executionId, taskIndex, "failed: " + e.getMessage());
request.getProgressCallback().accept(new ProgressUpdate<>(taskIndex, null, e));
throw e;
}
}));
}
Duration timeout = request.getTimeout();
Instant deadline = Instant.now().plus(timeout);
while (totalCompleted < subtasks.size() && Instant.now().isBefore(deadline)) {
try {
scope.joinUntil(Instant.now().plusMillis(50)); // Short polling interval for responsiveness; tune to avoid CPU spikes in high-task counts.
} catch (TimeoutException e) {
}
for (int i = 0; i < subtasks.size(); i++) {
if (!completed[i]) {
var subtask = subtasks.get(i);
var state = subtask.state();
switch (state) {
case SUCCESS:
case FAILED:
completed[i] = true;
totalCompleted++;
break;
case UNAVAILABLE:
break;
}
}
}
}
if (Instant.now().isAfter(deadline)) {
scope.shutdown();
}
}
Use short polling intervals and keep progress handlers lightweight. When run on virtual threads, parking during waits keeps carrier efficiency.
Nested scopes can mirror service boundaries cleanly.
public String executeHierarchical() throws Exception {
try (var parentScope = new StructuredTaskScope.ShutdownOnFailure()) {
var childTask1 = parentScope.fork(() -> executeChildTasks("Group-1"));
var childTask2 = parentScope.fork(() -> executeChildTasks("Group-2"));
var childTask3 = parentScope.fork(() -> executeChildTasks("Group-3"));
parentScope.join();
parentScope.throwIfFailed();
return String.format("Parent completed: [%s, %s, %s]",
childTask1.get(), childTask2.get(), childTask3.get());
}
}
private String executeChildTasks(String group) throws Exception {
try (var childScope = new StructuredTaskScope.ShutdownOnFailure()) {
var task1 = childScope.fork(() -> {
Thread.sleep(50);
return group + "-Task-1";
});
var task2 = childScope.fork(() -> {
Thread.sleep(100);
return group + "-Task-2";
});
childScope.join();
childScope.throwIfFailed();
return String.format("%s: [%s, %s]", group, task1.get(), task2.get());
}
}
Each scope has clear responsibility and failure boundary.
Sometimes parent response can proceed even when child enrichment scope fails.
public String bulkheadPattern() throws Exception {
try (var criticalScope = new StructuredTaskScope.ShutdownOnFailure();
var nonCriticalScope = new StructuredTaskScope.ShutdownOnFailure()) {
var criticalService1 = criticalScope.fork(() -> simulateServiceCall("critical-auth", 100));
var criticalService2 = criticalScope.fork(() -> simulateServiceCall("critical-payment", 150));
var nonCriticalService1 = nonCriticalScope.fork(() -> simulateServiceCall("analytics", 200));
var nonCriticalService2 = nonCriticalScope.fork(() -> simulateServiceCall("logging", 50));
criticalScope.join();
criticalScope.throwIfFailed();
try {
nonCriticalScope.join();
nonCriticalScope.throwIfFailed();
} catch (Exception e) {
// Non-critical scope failure caught separately for degraded response.
logger.warn("Non-critical services failed: {}", e.getMessage());
}
String result = String.format("Bulkhead Pattern: Critical[%s, %s] Non-Critical[%s, %s]",
criticalService1.get(), criticalService2.get(),
"analytics-ok", "logging-ok");
return result;
}
}
Keep fallback behavior explicit and product-reviewed.
joinUntil(...) loops can become busy waits if interval is too small; in Java 21 experiments, tight polling loops occasionally caused minor CPU spikes until intervals were tuned.throwIfFailed() when using ShutdownOnFailure and full-success semantics.For progressive/hierarchical flows, test:
javac --release 21 --enable-preview ...
java --enable-preview ...