Structured concurrency with Java 21 in 4 steps

Although concurrency is challenging, it is crucial for Software applications. Structured concurrency goes in the same direction as Java 21’s Virtual threads: making concurrent code more resource-efficient.

What is structured concurrency?

It is an approach to maintain the inherited scope of subtasks related to a parent task by ensuring every child subtask returns to the same place when they finish. This achieves better reliability and observability.

In OpenJDK, it is defined under JEP 453.

Why should I worry about (un)structured concurrency?

According to JEP 453, Structured concurrency in Java targets to:

Promote a style of concurrent programming that can eliminate common risks arising from cancellation and shutdown, such as thread leaks and cancellation delays.

To explain those two risks, let's consider the following scenario:

  • Low latency application

  • Fetches data from multiple sources (e.g., databases, HTTP APIs, remote cache)

  • Source requests are independent (no need to wait for the result of one to fulfill the other)

  • All requests must succeed; otherwise, the result fails

To achieve low latency, the application makes the source requests happen concurrently. This means that they will all happen simultaneously; therefore, the latency will be reduced as explained below:

  • Latency concurrent requests = max(latency source 1, latency source 2, …, latency source n); instead of

  • Latency sequential requests = (latency source 1 + latency source 2 + … + latency source n)

Thread leak ⚠️

In the best-case scenario, the application has considerably lower latency with no side effects. But what happens if one or multiple sources fail?

If all starts and one fails before the others complete, the thread that wraps all will be released without releasing the source requests threads, making you have orphan threads running (possibly indefinitely).

Therefore, in the worst-case scenario, the app has thread leaks ⚠️ (and Threads are expensive before Java 21’s Virtual threads).

Inefficient cancellation 🚫

So, in the worst-case scenario, the threads will become orphans. If cancellation mechanisms are in place, they will complete or wait for the cancellation signal (such as a timeout).

If the first request fails right away, the other ones will have to be fully processed or wait until a timeout (for example) is finished, although their processing is unnecessary.

How does it mitigate those risks?

The answer is a simple word: Scopes!

Instead of binding concurrent execution to an ExecutorService or ForkJoinPool, the StructuredConcurrency API allows the application to bind blocks of concurrent code to a scope.

As a result, every incomplete thread shuts down when the scope completes (or fails). There are currently two scopes available:

  • ShutdownOnSuccess: shuts down all threads after one succeeds or fails. This is appropriate when only the result of the first successful request is enough.

  • ShutdownOnFailure: shuts down all threads after one fails. This is appropriate when all requests must be successful.

The StructuredConcurrency API is responsible for the cancellation instead of leaving it to the developer(s) or framework.

Achieving structured concurrency in 4 steps:

Pre-requisites:

Download JDK 21 and update your project to Java 21

If you're starting a new project, Spring initalizr already has a template for JDK 21.

Enable preview

How to enable preview depends on how the application is being run. The goal is to run it with the flag --enable-preview .

To activate it in IntelliJ:

  1. Going into "Settings" > "Build, Execution, Deployment" > "Compiler" > "Java Compiler"

  2. Make sure the "Target bytecode version" is "21"

  3. In "Override compiler parameters per module," change the "Compilation options" to --enable-preview. If it doesn't work, add --relase 21, resulting in --release 21 --enable-preview.

Define logical scopes:

Before creating scopes in code, it's essential to find out in which parts of the business logic they fit.

Scopes work well for code that can run concurrently. Therefore, you should find pieces of logic that are independent between them but are required to fulfill the request. In our example, the business goal is to build a complete profile with basic user information, the most relevant posts, and follower count.

The input for each step is only the user identifier, making them independent. Perfect! Now it's time to code 🎉

Step 1. Creating the scope

Java 21 introduces as a preview feature the class StructuredTaskScope in package java.util.concurrent.

The user profile has the following behavior: All requests MUST succeed! Therefore, we will use the ShutdownOnFailure scope.

PS.: We will focus only on the method that uses the scopes. The implementations of the repository methods in the following snapshots are absent, but you can find the full code on github.

private UserCompleteProfile composeUserProfile(String userId) {
    try (final var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        // ...
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

The class ShutdownOnFailure implements (via StructuredTaskScope) AutoClosable, which means you don’t need to remember to close the scope 😎.

Step 2. Creating the Subtasks

// ...
// Inside the block of the scope
Subtask<List<Follower>> mostRelevantFollowersTask =
    scope.fork(() -> followersRepository.findFollowersByUserId(userId));
Subtask<UserFollowersCount> followersCountTask =
    scope.fork(() -> followersRepository.findFollowersCountByUserId(userId));
Subtask<UserInfo> userInfoTask =
    scope.fork(() -> userInfoRepository.findUserInfoByUserId(userId));

Each scope.fork creates a subtask that is bound to the scope. At this point, all tasks are created and running 🏃‍♀️.

Step 3. Waiting for all tasks to complete

// Wait until ALL are complete or at least one fails
scope.join();

Note that join is invoked in scope, not in subtask level.

Step 4. Getting the results

final var userInfo = userInfoTask.get();
final var mostRelevantFollowers = mostRelevantFollowersTask.get();
final var rawFollowersCount = followersCountTask.get();
final var followersCount = rawFollowersCount.followersCount();

final var profileResult = new UserCompleteProfile(
    userInfo.userId(),
    userInfo.username(),
    Period.between(LocalDate.now(), userInfo.birthDate()).getYears(),
    followersCount == null ? 0L : followersCount,
    mostRelevantFollowers
);

return profileResult;

You just achieved structured concurrency! Congratulations 👏

Limitations in Java 21

Structured concurrency is not production-ready in Java 21. There are some limitations that I found during my coding for this article:

  • It is a preview feature; therefore, it must enable the flag --enable-preview to use it. It's not something you want to do in production.

  • There are only two scope types available: ShutdownOnFailure and ShutdownOnSuccess

Structured concurrency and Java 21’s Virtual Threads

JEP 453 summarizes very well how structured concurrency is an excellent complement to virtual threads:

virtual threads deliver an abundance of threads. Structured concurrency can correctly and robustly coordinate them, and enables observability tools to display threads as they are understood by the developer

The application might have millions of concurrent threads that must be correctly coordinated.

Conclusion

Although Java 21 brings game-changing features, such as Virtual Threads, they're not enough to build resource-efficient applications.

Structured concurrency helps you develop more resilient and observable applications with the bonus of making logical blocks of asynchronous code more evident so you consider using it in your future projects!

I hope this was insightful, and please let a comment if you have doubts or remarks 😉

Thanks for reading.
Now let's get to know each other.

What we do

WAES supports organizations with various solutions at every stage of their digital transformation.

Discover solutions

Work at WAES

Are you ready to start your relocation to the Netherlands? Have a look at our jobs.

Discover jobs

Let's shape our future

Work at WAES

Start a new chapter. Join our team of Modern Day Spartans.

Discover jobs

Work with WAES

Camilo Parra Gonzalez

Camilo Parra Gonzalez

Account Manager