Modern Java + Concurrency

Structured Concurrency in Java

Structured concurrency treats related concurrent tasks as one unit of work, making cancellation, failure handling, and task ownership easier to reason about.

ConcurrencyStructured ConcurrencyModern JavaJava 21Virtual Threads

The Short Answer

Structured concurrency is a way to treat multiple related concurrent tasks as one unit of work.

Instead of starting child tasks and hoping you remember to wait, cancel, and handle failures correctly, structured concurrency gives those child tasks a clear parent scope.

The mental model is simple: if a parent operation starts several child tasks, those child tasks should finish, fail, or be cancelled before the parent operation is considered complete.

Version Note

Structured concurrency is a modern Java feature and has gone through preview iterations. Java 21 includes a preview version of StructuredTaskScope, and newer JDKs have continued to revise the API.

The concept is stable and important. The exact API syntax may depend on your JDK version, so check your target Java version before using it in production.

The Real Problem

Suppose a backend endpoint needs to build a dashboard. It needs user data, order data, and recommendation data.

text
Build Dashboard
    ├── Load user
    ├── Load orders
    └── Load recommendations

These calls can run concurrently. Starting them is not the hard part. The hard part is coordinating them correctly.

  • What if one task fails?
  • What if one task is slow?
  • What if the client disconnects?
  • What if the parent request times out?
  • Who cancels the remaining tasks?

Unstructured Concurrency Problem

With normal ExecutorService code, it is easy to start multiple tasks and accidentally manage them poorly.

java
Future<User> userFuture =
        executor.submit(() -> loadUser(userId));

Future<List<Order>> ordersFuture =
        executor.submit(() -> loadOrders(userId));

Future<List<Product>> recommendationsFuture =
        executor.submit(() -> loadRecommendations(userId));

User user = userFuture.get();
List<Order> orders = ordersFuture.get();
List<Product> recommendations = recommendationsFuture.get();

return new Dashboard(user, orders, recommendations);

This looks reasonable, but the failure behavior is awkward. If loading the user fails, what happens to the orders and recommendations tasks? Are they cancelled? Are they still running? Who owns them?

Unstructured concurrency often creates orphan work: tasks that keep running even after the parent operation no longer needs them.

Structured Concurrency Mental Model

Parent task: Build Dashboard
        |
        +-- child task: Load User
        |
        +-- child task: Load Orders
        |
        +-- child task: Load Recommendations

Parent does not finish until child tasks are handled.

The parent scope owns the child tasks. That gives the program a clearer lifecycle.

Without structure

Tasks can be started in different places and forgotten.

With structure

Related tasks are forked, joined, cancelled, and handled as a group.

Java 21 Style Example

This is the kind of code you may see with the Java 21 preview API.

java
import java.util.concurrent.StructuredTaskScope;

public Dashboard buildDashboard(String userId) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        var userTask =
                scope.fork(() -> loadUser(userId));

        var ordersTask =
                scope.fork(() -> loadOrders(userId));

        var recommendationsTask =
                scope.fork(() -> loadRecommendations(userId));

        scope.join();
        scope.throwIfFailed();

        return new Dashboard(
                userTask.get(),
                ordersTask.get(),
                recommendationsTask.get()
        );
    }
}

The important idea is not the exact syntax. The important idea is that the subtasks belong to the scope. The scope joins them, handles failure, and closes cleanly.

What Happens If One Task Fails?

Structured concurrency lets you choose a policy. A common policy is shutdown-on-failure.

text
Load user succeeds
Load orders fails
Load recommendations still running
Scope notices failure
Cancel remaining work
Parent fails cleanly
This is the big win: related concurrent tasks fail together instead of leaving random background work behind.

Why This Pairs Well With Virtual Threads

Structured concurrency and virtual threads are often discussed together.

Virtual threads make it cheap to run many blocking-looking tasks. Structured concurrency helps organize those tasks so their lifecycle stays understandable.

Virtual threads

Make concurrency cheaper and easier to scale for blocking-style code.

Structured concurrency

Makes groups of related concurrent tasks easier to manage safely.

Virtual threads answer: “Can I run many concurrent tasks cheaply?”

Structured concurrency answers: “Can I manage related concurrent tasks safely?”

Benefits

Clear ownership

Child tasks belong to a parent scope.

Better cancellation

If the parent fails or times out, child tasks can be cancelled together.

Cleaner error handling

Failures from related subtasks are handled as part of one operation.

Easier debugging

The runtime can show task relationships more clearly.

Less orphan work

Fewer background tasks continue running after the request no longer needs them.

Readable concurrency

Concurrent code starts looking more like normal scoped code.

When Would You Use It?

Structured concurrency is useful when one logical operation needs several concurrent subtasks.

  • Build a dashboard from multiple service calls
  • Fetch user, account, and permissions in parallel
  • Query multiple replicas and use the fastest successful result
  • Run independent validations concurrently
  • Call several downstream APIs as part of one request

When Not To Reach For It First

Structured concurrency is not a replacement for every async style.

Long-running background jobs

Queues, schedulers, and worker systems may be a better fit.

Event streams

Kafka-style pipelines usually have their own processing model.

Simple sequential code

If tasks do not need to run concurrently, keep the code simple.

Older Java versions

If your production runtime is Java 8, 11, or 17, this API is not available.

Interview-Friendly Explanation

Structured concurrency treats related concurrent tasks as one structured unit. A parent operation opens a scope, forks child tasks, waits for them, handles success or failure, and then closes the scope. This makes cancellation, failure handling, and task ownership much clearer than manually managing unrelated Future objects.

Common Interview Follow-Ups

Is structured concurrency the same as CompletableFuture?

No. CompletableFuture focuses on async composition and pipelines. Structured concurrency focuses on scoping and managing related concurrent tasks as one unit.

Why is it called structured?

Because concurrent subtasks follow the structure of the code. Child tasks are created inside a scope and must be handled before the scope exits.

What problem does it solve?

It helps prevent orphan tasks, unclear cancellation, scattered error handling, and confusing parent-child task lifecycles.

Is it production-ready in every Java version?

No. It is a modern preview API and has changed across JDK versions. Check the exact Java version and API status before using it in production.

How does it relate to virtual threads?

Virtual threads make it cheap to run many concurrent tasks. Structured concurrency helps organize those tasks so they are easier to cancel, join, and reason about.

Final Takeaway

Structured concurrency is about making concurrent code easier to reason about. If one request starts multiple related tasks, those tasks should have a clear parent scope, clear cancellation behavior, and clear failure handling.