🚀AdvancedModern Java

Structured Concurrency

Manage groups of concurrent tasks with StructuredTaskScope for reliable cancellation and error propagation in Java 21+.

Overview

Structured concurrency (Java 21 preview, becoming stable in later versions) treats a group of concurrent tasks as a unit with a defined lifetime: all subtasks start together and complete before the scope exits. This mirrors how structured programming (loops, functions) works for single-threaded code, bringing the same clarity to concurrent code. The key class is StructuredTaskScope. It opens a scope (try-with-resources), forks subtasks into it, and joins them. Two built-in policies are: - ShutdownOnFailure: cancel all remaining tasks if any task fails - ShutdownOnSuccess: cancel all remaining tasks as soon as one succeeds Structured concurrency prevents the common bugs of unstructured async code: forgetting to await a task, silently swallowed exceptions, and leaked threads.

Key Points

  • •StructuredTaskScope<T> manages a group of forked subtasks
  • •scope.fork(callable) submits a subtask and returns a Subtask<T>
  • •scope.join() waits for all subtasks or until a shutdown condition is met
  • •ShutdownOnFailure shuts down the scope if any subtask throws, then throwIfFailed() re-throws
  • •ShutdownOnSuccess shuts down as soon as any subtask returns successfully; result() retrieves the winner
  • •All forked subtasks are guaranteed to finish before the scope exits (even if cancelled)
  • •Subtask.get() retrieves the result; throws if the subtask failed or was cancelled
  • •Structured concurrency composes naturally with virtual threads
  • •Replaces error-prone CompletableFuture.allOf / anyOf patterns with explicit lifecycle management

Examples

ShutdownOnFailure: all tasks must succeed

import java.util.concurrent.*;

public class Main {
    static String fetchUser(int id) throws InterruptedException {
        Thread.sleep(50);
        return "User#" + id;
    }

    static String fetchOrders(int userId) throws InterruptedException {
        Thread.sleep(80);
        return "Orders for " + userId;
    }

    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            StructuredTaskScope.Subtask<String> user   = scope.fork(() -> fetchUser(42));
            StructuredTaskScope.Subtask<String> orders = scope.fork(() -> fetchOrders(42));

            scope.join();           // wait for both
            scope.throwIfFailed();  // re-throw if any failed

            System.out.println(user.get());
            System.out.println(orders.get());
        }
    }
}

Both tasks run in parallel. If either throws, throwIfFailed() cancels the other and propagates the exception. Without structured concurrency, you would need CompletableFuture.allOf() with manual exception unwrapping.

ShutdownOnSuccess: first successful result wins

import java.util.concurrent.*;

public class Main {
    static String fetchFromPrimary() throws InterruptedException {
        Thread.sleep(200); // slower
        return "result from primary";
    }

    static String fetchFromFallback() throws InterruptedException {
        Thread.sleep(50); // faster
        return "result from fallback";
    }

    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
            scope.fork(Main::fetchFromPrimary);
            scope.fork(Main::fetchFromFallback);

            scope.join(); // returns when first succeeds or both fail

            String result = scope.result(); // the winner's result
            System.out.println("Got: " + result); // result from fallback
        }
    }
}

ShutdownOnSuccess is perfect for hedged requests: send the same request to multiple servers and use the first successful response. The slower task is cancelled automatically when the faster one returns.

Nested scopes and composition

import java.util.concurrent.*;

public class Main {
    record UserProfile(String name, String preferences) {}

    static String fetchName(int id) throws InterruptedException {
        Thread.sleep(40);
        return "Alice";
    }

    static String fetchPreferences(int id) throws InterruptedException {
        Thread.sleep(60);
        return "dark-mode, notifications-on";
    }

    static UserProfile fetchProfile(int id) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            var name  = scope.fork(() -> fetchName(id));
            var prefs = scope.fork(() -> fetchPreferences(id));
            scope.join().throwIfFailed();
            return new UserProfile(name.get(), prefs.get());
        }
    }

    public static void main(String[] args) throws Exception {
        UserProfile profile = fetchProfile(1);
        System.out.println("Name: " + profile.name());
        System.out.println("Prefs: " + profile.preferences());
    }
}

Nested scopes compose cleanly. fetchProfile opens its own scope, forking two tasks. If the caller also runs in a scope, failures still propagate correctly through the hierarchy. Each scope is a self-contained unit of work.

Common Mistakes

  • Calling subtask.get() before scope.join(). Subtask.get() throws IllegalStateException if the task has not yet completed. Always call scope.join() (and throwIfFailed() if using ShutdownOnFailure) before accessing results.
  • Not using throwIfFailed() with ShutdownOnFailure. join() returns even if tasks failed. Without throwIfFailed(), you silently get cancelled subtasks and calling get() on them throws.
  • Treating StructuredTaskScope like an ExecutorService and submitting unbounded work without join(). The scope is designed for bounded task groups. For unbounded streaming work, use ExecutorService.

Note

Structured concurrency pairs perfectly with virtual threads (Executors.newVirtualThreadPerTaskExecutor is the default executor for StructuredTaskScope). You get both the high concurrency of virtual threads and the structured lifetime management of scoped tasks.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

What is the key guarantee that StructuredTaskScope provides?