☕ Java

Structured Concurrency

Structured concurrency, delivered as an incubating/preview API across many JDK releases starting with JEP 428 in Java 19 and still in preview as of Java 25's JEP 505 (continuing to evolve in subsequent releases), provides the StructuredTaskScope class to treat a group of related concurrent subtasks — typically run on virtual threads — as a single, lexically-scoped unit of work, where every forked subtask is guaranteed to complete (or be cancelled) before the scope itself exits, and errors from any subtask are handled centrally rather than needing to be separately propagated and reasoned about. This directly addresses the reliability and observability problems of unstructured concurrency, where a plain ExecutorService or raw thread creation allows subtasks to silently outlive the logical operation that spawned them, with no enforced relationship between a task's lifetime and its subtasks' lifetimes. This entry covers exactly what unstructured concurrency gets wrong and why virtual threads made the problem more visible, the StructuredTaskScope.open()/fork()/join() lifecycle and Joiner-based completion policies, and how subtask cancellation and the strict thread-ownership/nesting rules work. As of Java 25, this remains a preview feature requiring --enable-preview, with the public-constructor-based API of earlier previews replaced by static factory methods.

Motivation — What Unstructured Concurrency Gets Wrong

An ordinary ExecutorService, or a thread started directly, has no enforced relationship between the lifetime of the logical operation that created it and the lifetime of the thread itself: a thread submitted to an executor can easily outlive the method that submitted it, continue running in the background with no remaining reference connecting it to "the operation that needed it," and there is nothing in the language or the executor API that prevents this — it's a pattern the programmer must maintain entirely through discipline, and one that's easy to get wrong, especially once error handling and early-return paths are involved. If a method forks off several concurrent subtasks to fetch different pieces of data and one of those subtasks throws an exception, nothing automatically cancels the others; they continue running, potentially performing unnecessary work, holding resources, or even writing results nobody will ever consult, unless the programmer remembers to manually track every forked task and explicitly cancel the rest whenever one fails — logic that has to be hand-written and correctly maintained at every single call site that forks concurrent subtasks, with no help from the platform itself. Virtual threads, by making it practical to create many thousands or millions of lightweight threads cheaply, made this existing structural problem considerably more pressing in practice: a codebase that previously created at most a few dozen carefully-managed platform threads, where any lingering or leaked thread was a noticeable, rare event, can with virtual threads spawn enormous numbers of them as a matter of routine, and the same lack of structural enforcement that was a latent risk with platform threads becomes a much more immediate and visible risk of orphaned, leaked, or silently-still-running virtual threads if the same unstructured patterns are simply carried over unchanged. Structured concurrency, conceived alongside virtual threads as part of Project Loom, directly answers this by making the relationship between a task and its subtasks an explicit, enforced, lexical-scope-based structure: subtasks are forked only within a clearly bounded block, and that block's own exit (whether by normal completion or by exception) is guaranteed to mean every subtask has also completed or been cancelled — exactly the same way a method call's own local variables and stack frame cannot outlive the method call itself.
Java
// ── Unstructured concurrency — nothing enforces subtask lifetime/cleanup
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<String> userFuture = executor.submit(() -> fetchUser(userId));
Future<List<Order>> ordersFuture = executor.submit(() -> fetchOrders(userId));

try {
    String user = userFuture.get();          // if THIS throws...
    List<Order> orders = ordersFuture.get();   // ...ordersFuture's task keeps running anyway —
    return combine(user, orders);                // nothing automatically cancelled it
} catch (Exception e) {
    // Must manually remember to cancel the OTHER future here, at every call site like this:
    ordersFuture.cancel(true);
    throw e;
}
// Easy to forget this manual cancellation — and even when remembered, it has to be
// hand-written and correctly maintained separately at every concurrent call site

// ── Structured concurrency — the scope's own exit GUARANTEES subtask cleanup
// (preview feature — requires --enable-preview as of Java 25)
String result;
try (var scope = StructuredTaskScope.open()) {
    StructuredTaskScope.Subtask<String> user = scope.fork(() -> fetchUser(userId));
    StructuredTaskScope.Subtask<List<Order>> orders = scope.fork(() -> fetchOrders(userId));

    scope.join();   // waits for both — if EITHER fails, the other is automatically cancelled,
                      // and join() throws — no manual cancellation logic needed anywhere

    result = combine(user.get(), orders.get());
}
// By the time this try-with-resources block exits, BOTH subtasks are guaranteed to have
// either completed or been cancelled — no thread is ever left running in the background

The open()/fork()/join() Lifecycle and Joiner-Based Completion Policies

A StructuredTaskScope is opened via a static factory method — StructuredTaskScope.open() for the common, default policy, or the one-argument open(joiner) overload to supply a custom completion policy — rather than via a public constructor, a deliberate API design (settled on during the API's later previews) that keeps sensible defaults concise while leaving room for the implementation to evolve without breaking existing calling code. The thread that opens the scope becomes that scope's owner, and only the owner is permitted to fork subtasks into it or call join() on it — calling fork() from any thread other than the scope's owner is rejected with an exception at run time, a structural rule enforced by the API itself rather than left to convention. Each call to fork(callable) starts a new subtask, by default running on its own virtual thread, and immediately returns a Subtask<T> handle (not a Future, in the now-settled API — Future was deliberately not reused here, since Future's broader feature set, designed for arbitrary asynchronous composition, offers no benefit for subtasks that are only ever inspected after the scope's own join() has already confirmed they're complete, and could actively mislead callers into using it in ways structured concurrency doesn't intend). After forking all of a scope's subtasks, the owner calls join(), which blocks until either every subtask completes according to the scope's configured policy, or the scope is cancelled; calling Subtask::get afterward retrieves a successfully completed subtask's result. The completion policy — what should happen when subtasks succeed or fail relative to each other — is expressed via a Joiner, passed to the one-argument open(joiner) factory method. The zero-argument open() implicitly uses the default Joiner, which fails fast: if any subtask fails, every other still-running subtask in the scope is cancelled, and join() itself throws, surfacing that single failure rather than silently continuing with incomplete data. The Joiner interface itself exposes onFork() and onComplete() callback methods plus a result() method, and the JEP provides several ready-made Joiner factory methods for the most common policies beyond the fail-fast default — notably one that waits for the first subtask to succeed (treating the rest as no-longer-needed and cancelling them), useful for invoke-several-redundant-sources-and-take-whichever-responds-first patterns, and one that collects every subtask's outcome (success or failure) without cancelling anything on a single failure, for cases where partial results are still useful.
Java
// (preview feature — requires --enable-preview as of Java 25; exact API may still evolve)

// ── open() — the default, fail-fast policy ──────────────────────────────
try (var scope = StructuredTaskScope.open()) {
    StructuredTaskScope.Subtask<String> productInfo = scope.fork(() -> fetchProductInfo(productId));
    StructuredTaskScope.Subtask<Integer> inventory = scope.fork(() -> fetchInventory(productId));
    StructuredTaskScope.Subtask<Double> price = scope.fork(() -> fetchPersonalizedPrice(userId, productId));

    scope.join();   // default policy: if ANY subtask fails, the others are cancelled
                      // and join() throws — surfaces the failure, doesn't silently continue

    // Reaching here means ALL THREE subtasks succeeded:
    return new ProductPage(productInfo.get(), inventory.get(), price.get());
}

// ── Only the scope's OWNER thread may fork or join — enforced at runtime ─
try (var scope = StructuredTaskScope.open()) {
    Thread.ofVirtual().start(() -> {
        // scope.fork(() -> "illegal");   // StructureViolationException — wrong thread
    }).join();
}

// ── Custom Joiner — e.g. "first successful result wins, cancel the rest"
// Used for redundant-source patterns: query several equivalent backends,
// take whichever responds first, cancel the no-longer-needed remainder:
try (var scope = StructuredTaskScope.open(StructuredTaskScope.Joiner.<String>anySuccessfulResultOrThrow())) {
    scope.fork(() -> queryPrimaryRegion());
    scope.fork(() -> querySecondaryRegion());
    scope.fork(() -> queryTertiaryRegion());

    String fastestResult = scope.join();   // returns as soon as ONE succeeds;
    // the still-running others are automatically cancelled at that point
    return fastestResult;
}

Cancellation, Nesting, and Strict Structural Enforcement

When a scope's configured policy determines the scope should be cancelled — the default fail-fast Joiner triggering on a subtask failure, or any custom Joiner's onComplete() logic signalling the same — every other subtask still running within that scope is interrupted, and the scope's owner, if it is blocked inside join() at the time, is released from that wait once cancellation and cleanup have actually completed; if instead the owner's code somehow exits the scope's block before calling join() at all (an early return or an exception thrown before reaching join()), the scope is likewise cancelled, and the owner will block inside the scope's own close() method (invoked automatically by the try-with-resources block) until every forked subtask has actually terminated — guaranteeing that no subtask thread is ever left running, unobserved, past the point where the scope's block has exited, regardless of which exit path was taken. A subtask running inside a scope can itself open its own nested StructuredTaskScope to fork further subtasks of its own, creating a hierarchy of scopes whose nesting directly mirrors the code's own block structure — and this hierarchy is exactly what gives structured concurrency its observability benefit: because every scope's parent-child relationship is reflected in actual lexical nesting rather than scattered, ad hoc executor submissions, a thread dump can display that hierarchy directly, letting a developer (or an automated tool) see at a glance which threads belong to which logical operation, dramatically simplifying the kind of "which background thread does this orphaned-looking stack trace actually belong to" investigation that unstructured thread pools have always made tedious. The API enforces this structure strictly at run time, not merely as a documented convention: using a scope outside the try-with-resources block that opened it, or violating the proper nesting of close() calls between an outer and inner scope, throws a StructureViolationException, specifically to prevent exactly the kind of accidental "fork-bomb" or thread-leak pattern that unstructured concurrency made easy to introduce by accident — the strictness is deliberate, trading some flexibility for a much stronger, automatically-checked guarantee that concurrent code organized this way genuinely cannot leak threads past the scope meant to own them.
Java
// ── Cancellation propagates automatically to sibling subtasks on failure ─
try (var scope = StructuredTaskScope.open()) {
    var risky = scope.fork(() -> { throw new RuntimeException("failed"); });
    var slow = scope.fork(() -> {
        Thread.sleep(Duration.ofSeconds(30));   // this subtask gets INTERRUPTED/cancelled
        return "never reached";                   // as soon as "risky" fails above
    });

    scope.join();   // throws here, surfacing "risky"'s failure — "slow" has ALREADY
                      // been cancelled by this point, not left running in the background
}

// ── Nested scopes — hierarchy mirrors the code's own block structure ───
try (var outerScope = StructuredTaskScope.open()) {
    var aggregateTask = outerScope.fork(() -> {
        try (var innerScope = StructuredTaskScope.open()) {
            var sub1 = innerScope.fork(() -> fetchPartA());
            var sub2 = innerScope.fork(() -> fetchPartB());
            innerScope.join();
            return combine(sub1.get(), sub2.get());
        }
        // innerScope's subtasks are GUARANTEED done by this point — nested,
        // matching the lexical nesting of outerScope -> aggregateTask -> innerScope
    });
    outerScope.join();
    System.out.println(aggregateTask.get());
}
// A thread dump taken during this execution shows the full scope hierarchy directly —
// outerScope's subtask, and innerScope nested beneath it, with their forked threads —
// rather than a flat, unstructured list of unrelated-looking thread names

// ── Strict structural enforcement — violations throw at run time ──────
// StructuredTaskScope leaked = ...;
// void useOutsideItsBlock() {
//     leaked.fork(() -> "too late");   // StructureViolationException —
//     // using a scope outside the try-with-resources block that opened it
//     // is rejected, not merely discouraged by convention
// }

Related Topics in Java 9 to 21 Features

Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.
JShell
JShell, introduced in Java 9, is an interactive Read-Eval-Print Loop (REPL) bundled directly with the JDK, allowing Java statements, expressions, method, class, and variable declarations to be entered and evaluated one at a time at an interactive prompt, with immediate feedback — without requiring a class wrapper, a main method, explicit compilation, or a build tool, all of which Java had always required for even the smallest snippet of executable code before Java 9. This solved a long-standing friction point for learning, quick experimentation, and API exploration, where languages like Python or Ruby offered an immediate interactive prompt but Java required the full edit-compile-run cycle even to test a single line. This entry covers what problem JShell solves and how it differs from a script, core REPL usage including implicit variable creation and forward references, snippet management commands, and how to feed JShell external classes and files.
Collection Factory Methods
Collection factory methods — List.of(...), Set.of(...), and Map.of(...)/Map.ofEntries(...) — introduced in Java 9, provide a concise, standard way to create small, fixed-content, genuinely immutable collections in a single expression, replacing the previously common but verbose combination of Arrays.asList(...), Collections.unmodifiableList(...), and manual add() calls, none of which produced a collection that was both concise to write and fully immutable. This entry covers exactly what problem these factories solve compared to the older idioms, the specific immutability and null-rejection guarantees they provide (which are stricter than Collections.unmodifiableXxx), their behavior around duplicate keys/elements, and important caveats around mutability expectations and performance for very small collections.
Private Interface Methods
Private interface methods, introduced in Java 9, allow an interface to declare methods marked private — with a full method body — that are accessible only from within that same interface (from its default methods, its other private methods, and its private static methods), but completely invisible to implementing classes and to any code outside the interface. They close a code-duplication gap left after Java 8's default methods: when an interface has multiple default methods that share common logic, that shared logic previously either had to be duplicated in each default method or exposed as a public default method purely to enable reuse, polluting the interface's actual API surface with a method meant only for internal reuse. This entry covers the specific problem they solve, the distinction between private instance and private static interface methods, the rules governing what they can call and be called from, and how they interact with the broader default-method design Java established in version 8.