Structured Concurrency and Scoped Values in Java

Mon, 01 Sep 2025 12:27:00 +0000
Java, structured concurrency, project loom, uncoverjava, java 25

The work on JEP-505 resulted in the introduction of structured concurrency in Java 25. In this article, updated in September 2025, we will delve into the concept of structured concurrency, compare it with the current API, and explore the problems it solves.

Join us on a journey through decades of Java evolution

What is structured concurrency

Structured concurrency means that all subtasks are bound to the scope of their parent task and cannot outlive it, just like a method call cannot last longer than the method that invoked it.

It makes it much easier to reason about concurrent code: no stray background threads continue running after the parent has finished, error handling is more predictable since failures can be managed in one place, and the overall system is simpler to observe and debug.

Check:

Programmer-Friendly Structured Concurrency for Java

Threads, ThreadPools & Executors: Multi Thread Processing In Java

In practice, it prevents the typical pitfalls of uncontrolled concurrency, like thread leaks or tasks lingering indefinitely, often leading to fragile and hard-to-maintain systems.

In Java, structured concurrency can be used with virtual threads from Project Loom. Because virtual threads are extremely lightweight, creating new tasks is almost free, making this approach both practical and efficient even in highly concurrent applications.

Example: invoice generation with structured concurrency

To illustrate the issues with the current API, let’s consider a simple example: generating an invoice. We must fetch the issuer, the customer, and the list of items. Doing this sequentially on a single thread works, but it’s slow. A better approach would be to fetch the data in parallel.

Let’s first see how this looks with ExecutorService.

(Un)structured concurrency

Invoice generateInvoice() throws ExecutionException, InterruptedException {
    try (final var executor = Executors.newFixedThreadPool(3)) {
        Future<Issuer> issuer = executor.submit(this::findIssuer);
        Future<Customer> customer = executor.submit(this::findCustomer);
        Future<List<Item>> items = executor.submit(this::findItems);

        return new Invoice(issuer.get(), customer.get(), items.get());
    }
}

This approach brings with it a couple of problems:

When findCustomer takes longer to execute than findItems, and findItems throws an exception,
code will be blocked at customer.get(), until the thread executing findCustomer ends. As a result, we will waste resources waiting for the subtask to finish.

If the thread calling generateInvoice() is interrupted, the subtasks in the executor are not interrupted and get leaked.
They can even operate indefinitely without control.

Structured concurrency

Invoice generateInvoice() throws InterruptedException {
    try (var scope = StructuredTaskScope.open(
            StructuredTaskScope.Joiner.allSuccessfulOrThrow() // (1)
    )) {
        Subtask<Issuer> issuer = scope.fork(this::findIssuer);
        Subtask<Customer> customer = scope.fork(this::findCustomer);
        Subtask<List<Item>> items = scope.fork(this::findItems);

        scope.join(); // (2)

        return new Invoice(issuer.get(), customer.get(), items.get());
    }
}

(1): We create a new StructuredTaskScope using the factory method open() with the policy allSuccessfulOrThrow().
If an exception is thrown by findIssuer, findCustomer, or findItems, the other subtasks will be canceled and the scope fails.

(2): Waits for all subtasks in scope to be finished and throws an exception on any failure.

The code above is free of the problems mentioned earlier:

When findCustomer takes longer to execute than findItems, but findItems throws...