Modern Java

38. Structured Concurrency

Structured concurrency (StructuredTaskScope, Java 21+) treats a group of related concurrent tasks as a single unit of work, so failures and cancellations propagate cleanly and threads don't leak.

Java 21+ (preview โ†’ finalized in later versions)

Explanation

Structured concurrency (JEP 453) ensures that the lifetime of concurrent tasks is scoped to a structured block. When the block exits, all subtasks are guaranteed to have completed (or been cancelled). This eliminates the common bugs of unstructured concurrency: tasks that outlive their context, leaked threads, and partial failures that leave state inconsistent.

StructuredTaskScope.ShutdownOnFailure cancels all remaining subtasks as soon as any one fails, then re-throws the exception in the parent. StructuredTaskScope.ShutdownOnSuccess cancels remaining tasks as soon as one succeeds (useful for racing alternatives).

The pattern: open a scope, fork subtasks, join (wait for completion policy), check results/exceptions, use results. All inside a try-with-resources block so the scope is always closed.

Example

import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;

public class StructuredConcurrencyDemo {
    record UserProfile(String name, String email) {}
    record UserOrders(int count) {}

    static UserProfile  fetchProfile(long userId) throws InterruptedException {
        Thread.sleep(50); // simulate DB call
        return new UserProfile("Alice", "alice@example.com");
    }

    static UserOrders fetchOrders(long userId) throws InterruptedException {
        Thread.sleep(80); // simulate DB call
        return new UserOrders(42);
    }

    static void handleRequest(long userId) throws Exception {
        // ShutdownOnFailure: if either subtask fails, cancel the other
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            Subtask<UserProfile> profileTask = scope.fork(() -> fetchProfile(userId));
            Subtask<UserOrders>  ordersTask  = scope.fork(() -> fetchOrders(userId));

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

            // Both succeeded โ€” safe to call .get()
            UserProfile profile = profileTask.get();
            UserOrders  orders  = ordersTask.get();

            System.out.printf("User: %s, Orders: %d%n", profile.name(), orders.count());
        }
    }

    public static void main(String[] args) throws Exception {
        handleRequest(1L);
    }
}

Sources