Modern Java Evolution

Structured Concurrency & Scoped Values

Structured concurrency (finalized in Java 25) makes concurrent subtasks children of a scope: they complete, fail, or cancel together, with errors propagating like ordinary exceptions. Scoped values replace ThreadLocal for context in virtual-thread code.
  • StructuredTaskScope forks subtasks and joins them as a unit — no orphaned threads
  • Joiner policies: all-succeed-or-throw, first-success wins (anySuccessful), custom
  • Failure of one subtask cancels the siblings — timeouts and errors stop wasted work
  • The task tree is visible in thread dumps — concurrency you can see
  • ScopedValue: immutable, inheritance-friendly context — the ThreadLocal successor
  • Both finalized in Java 25 (previews through 21–24)
Fork two calls, need both (Java 25 API)
Response handle(Request req) throws InterruptedException {
    try (var scope = StructuredTaskScope.open()) {          // all must succeed
        Subtask<User> user = scope.fork(() -> findUser(req.userId()));
        Subtask<Order> order = scope.fork(() -> fetchOrder(req.orderId()));

        scope.join();                                        // waits; throws if any failed,
                                                              // cancelling the sibling
        return new Response(user.get(), order.get());
    }   // leaving the scope guarantees no subtask outlives it
}

The problem it fixes: with bare executors or Completable Future, a failed subtask's siblings keep burning resources, cancellation must be wired by hand, and thread dumps show an unstructured soup. Structured concurrency imports the discipline of structured programming — a concurrent operation has one entry, one exit, and its children are lexically scoped. join() is mandatory before reading results; forgetting it is an error, not a race.

ScopedValue: context without the ThreadLocal foot-guns
private static final ScopedValue<RequestContext> CONTEXT = ScopedValue.newInstance();

ScopedValue.where(CONTEXT, new RequestContext(traceId, user))
           .run(() -> handleRequest(req));      // bound for this call tree only

// anywhere below — including forked subtasks, which inherit it automatically:
var ctx = CONTEXT.get();

ThreadLocal's problems compound with Virtual Threads: unbounded mutability, leaks when threads are pooled, expensive per-thread copies for inheritance across millions of threads. ScopedValue is immutable, bound for a dynamic scope, automatically inherited by StructuredTaskScope forks, and freed on scope exit — request context (trace IDs, auth) done right (Observability).

Sources
  • Core Java, Volume I: Fundamentals (13th ed.)Ch. 10 — Concurrency (structured concurrency preview)
  • Optimizing Cloud Native Java (2nd ed.)Ch. 15 — Modern Performance and The Future