Java 26: Structured Concurrency (JEP 525 — Preview)

Concurrent programming in Java just got a lot safer. JEP 525 continues Structured Concurrency as a preview API that treats groups of related concurrent tasks as a single unit of work with automatic lifecycle management.

Status: Sixth Preview — requires --enable-preview

The Problem with ExecutorService

Traditional concurrent code has several issues:

  • No automatic cancellation of other tasks when one fails
  • No structured lifetime — threads can outlive the scope
  • Hard to reason about error handling
  • Thread dumps don’t show task relationships

The Solution: StructuredTaskScope

try (var scope = StructuredTaskScope.open()) {
    var user  = scope.fork(() -> fetchUser());
    var order = scope.fork(() -> fetchOrder());
    scope.join();         // Wait for all
    use(user.get(), order.get());
}   // All tasks guaranteed complete here

When the scope closes, all tasks are guaranteed to be complete. If one fails, others can be automatically cancelled.

Key Joiners

  • awaitAll() — Wait for all tasks, regardless of outcome
  • awaitAllSuccessfulOrThrow() — Wait for all; throw on first failure
  • allSuccessfulOrThrow() — Wait for all; return a List of results
  • anySuccessfulOrThrow() — Return first success, cancel the rest
  • allUntil(Predicate) — Wait until predicate matches a result

Demo Highlights

1. Fork/Join All — Concurrent Data Fetching

Three simulated service calls run concurrently as virtual threads. Total time is the max of the individual calls, not the sum:

try (var scope = StructuredTaskScope.open()) {
    var userTask  = scope.fork(() -> fetchUser());
    var orderTask = scope.fork(() -> fetchOrder());
    var recsTask  = scope.fork(() -> fetchRecommendations());

    scope.join();

    String user  = userTask.get();
    String order = orderTask.get();
    String recs  = recsTask.get();
    // ~200ms total (slowest fork), not 150+200+100=450ms
}

2. Race — First Successful Result Wins

anySuccessfulOrThrow() returns as soon as one task succeeds, automatically cancelling the remaining tasks:

try (var scope = StructuredTaskScope.open(
        StructuredTaskScope.Joiner.<String>anySuccessfulOrThrow())) {

    scope.fork(() -> queryMirror("US-East",  200));
    scope.fork(() -> queryMirror("EU-West",  100));  // Fastest!
    scope.fork(() -> queryMirror("AP-South", 300));

    String result = scope.join();  // Returns ~100ms after scope open
}

3. Failure Handling — Automatic Cancellation

When one subtask throws an exception, awaitAllSuccessfulOrThrow() automatically cancels the remaining tasks and propagates the error:

try (var scope = StructuredTaskScope.open(
        StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow())) {

    scope.fork(() -> { Thread.sleep(100); return "Task A done"; });
    scope.fork(() -> { Thread.sleep(50);  throw new RuntimeException("Task B failed!"); });
    scope.fork(() -> { Thread.sleep(200); return "Task C done"; });

    scope.join();
} catch (Exception e) {
    // Task B failed → Tasks A and C automatically cancelled
    System.out.println("Caught failure: " + e.getMessage());
}

Real-World Use Cases

The companion StructuredConcurrencyRealWorldExamples class demonstrates six production scenarios:

API Aggregator (Backend-for-Frontend)

A mobile app’s BFF assembles a dashboard from 4 microservices concurrently. All tasks are bounded to the scope — if any critical service fails, the others are cancelled:

try (var scope = StructuredTaskScope.open()) {
    var userTask  = scope.fork(() -> fetchService("UserService", 80, "User{Alice, premium}"));
    var orders    = scope.fork(() -> fetchService("OrderService", 120, "Orders[3 pending]"));
    var notifs    = scope.fork(() -> fetchService("NotificationService", 60, "Notifs[5 unread]"));
    var recs      = scope.fork(() -> fetchService("RecommendationService", 100, "Recs[10 items]"));

    scope.join();

    // Sequential: ~360ms. Concurrent: ~120ms (slowest fork only)
    var dashboard = new DashboardData(
        userTask.get(), orders.get(), notifs.get(), recs.get());
}

Price Comparison (Race Pattern)

Query multiple suppliers for the same product — return the fastest response, cancel the rest. Used by Google Flights, Kayak, and insurance quote aggregators:

record Quote(String supplier, double price, long latencyMs) {}

try (var scope = StructuredTaskScope.open(
        StructuredTaskScope.Joiner.<Quote>anySuccessfulOrThrow())) {

    scope.fork(() -> { Thread.sleep(200); return new Quote("SupplierA", 29.99, 200); });
    scope.fork(() -> { Thread.sleep(80);  return new Quote("SupplierB", 31.99, 80); });
    scope.fork(() -> { Thread.sleep(150); return new Quote("SupplierC", 27.99, 150); });

    Quote fastest = scope.join();
    // SupplierB wins in ~80ms; SupplierA and SupplierC are cancelled
}

Health Check Dashboard (Fan-Out to All Services)

Unlike the race pattern, here every result matters — even failures. All health checks run in parallel:

try (var scope = StructuredTaskScope.open()) {
    var auth     = scope.fork(() -> checkHealth("auth-service", 50, true));
    var payments = scope.fork(() -> checkHealth("payment-service", 80, true));
    var catalog  = scope.fork(() -> checkHealth("catalog-service", 120, false)); // down!
    var search   = scope.fork(() -> checkHealth("search-service", 40, true));

    scope.join();

    List<HealthResult> results = List.of(
        auth.get(), payments.get(), catalog.get(), search.get());
    long healthy = results.stream().filter(HealthResult::healthy).count();
    System.out.println(healthy + "/" + results.size() + " services healthy");
}

Payment with Fallback

Try the primary processor (Stripe). On failure, automatically retry with the fallback (PayPal). Structured concurrency ensures no zombie threads:

try (var scope = StructuredTaskScope.open(
        StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow())) {
    scope.fork(() -> {
        Thread.sleep(100);
        throw new RuntimeException("Stripe gateway timeout");
    });
    scope.join();
    return "Paid via Stripe";

} catch (Exception primaryFailure) {
    // Fallback scope
    try (var fallback = StructuredTaskScope.open()) {
        var result = fallback.fork(() -> {
            Thread.sleep(80);
            return "PayPal txn-" + UUID.randomUUID().toString().substring(0, 8);
        });
        fallback.join();
        return "✅ Paid via PayPal: " + result.get();
    }
}

Timeout-Bounded Fetch (Hard Deadline)

Enforce a deadline on a slow service. No leaked threads — structured concurrency guarantees cleanup:

try (var scope = StructuredTaskScope.open(
        StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow(),
        cf -> cf.withTimeout(Duration.ofMillis(200)))) {

    var task = scope.fork(() -> {
        Thread.sleep(serviceLatencyMs);
        return "Service responded";
    });
    scope.join();
    return "✅ " + task.get();

} catch (Exception e) {
    return "⏱️ Timed out after 200ms → using fallback";
}

Other Use Cases in the Demo

  • Parallel search — search database, Elasticsearch, and external API concurrently; merge results

Running the Demo

mvn compile exec:exec \
  -Dexec.mainClass=org.example.standard.StructuredConcurrencyDemo

Check out the full source on GitHub.