Understanding Structured Concurrency in Java (New preview feature in Java 21)
🚀 Introduction
Java 19 introduced Structured Concurrency (preview feature) to simplify and enhance concurrent programming. In the latest version of Java that is 23 its still in preview. While traditional concurrency models require manual thread management, structured concurrency automatically handles task lifecycles and improves readability, reliability, and performance.
In this guide, we’ll explore:
- What Structured Concurrency is and why it matters.
- How to use it in a Spring Boot application.
- Handling failures gracefully when calling multiple external services.
- A fully working implementation with code examples.
🌟 What is Structured Concurrency?
Structured Concurrency is a programming paradigm that helps manage concurrent tasks in a structured, predictable manner. Java introduced StructuredTaskScope
in JDK 19 (as a preview feature) to make concurrent programming more manageable by ensuring that child tasks are properly tracked and completed before the parent task exits.
Benefits of Structured Concurrency:
- Improved Readability & Maintainability — Helps in writing clear and structured concurrent code where parent-child relationships are explicit.
- Automatic Cancellation & Exception Handling — If one task fails, other related tasks can be canceled automatically, preventing resource leaks.
- Better Debugging & Observability — All tasks within a scope are monitored, making debugging easier.
- Avoids Orphaned Tasks — Ensures that background tasks don’t continue running when the parent thread exits.
- Efficient Resource Management — Prevents unnecessary thread usage and improves system stability.
Evaluating if Structured Concurrency is Useful in Your Code:
- Check for Unstructured Forking of Threads — If your code uses
ExecutorService
,CompletableFuture
, or manually createdThread
instances, structured concurrency might simplify it. - Look for Cancellation & Exception Handling Issues — If you have trouble managing cancellations or catching exceptions from multiple threads,
StructuredTaskScope
can help. - Identify Thread Leaks or Resource Waste — If background tasks continue running even after the main process has stopped, structured concurrency ensures proper termination.
- Assess Readability & Debugging Complexity — If your concurrent code is hard to debug or follow, restructuring with
StructuredTaskScope
can make it clearer.
🛠️ How to Enable Structured Concurrency in IntelliJ
Since this feature is still in preview, we need to enable it explicitly.
Step 1: Use Java 21+
Ensure you’re using Java 21 as your project SDK.
Step 2: Enable Preview Features
In IntelliJ IDEA, add the --enable-preview
flag:
- Go to Run → Edit Configurations.
- Under Build and run, add
--enable-preview
in the VM Options field. - Click Apply → OK.
Now, you’re ready to use Structured Concurrency! 🎉
🔄 Traditional Concurrency vs. Structured Concurrency
🔹 Traditional Approach (ExecutorService)
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<Integer> price1 = executor.submit(() -> fetchPrice("Airline1"));
Future<Integer> price2 = executor.submit(() -> fetchPrice("Airline2"));
Future<Integer> price3 = executor.submit(() -> fetchPrice("Airline3"));
List<Integer> prices = List.of(price1.get(), price2.get(), price3.get());
executor.shutdown();
❌ Issues:
- Manually manages thread pools.
- Hard to cancel ongoing tasks if one fails.
- Requires explicit shutdown to avoid thread leaks.
✅ Structured Concurrency Approach
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var price1 = scope.fork(() -> fetchPrice("Airline1"));
var price2 = scope.fork(() -> fetchPrice("Airline2"));
var price3 = scope.fork(() -> fetchPrice("Airline3"));
scope.join(); // Wait for all tasks
scope.throwIfFailed(); // Throw exception if any task fails
return List.of(price1.get(), price2.get(), price3.get());
}
✅ Benefits:
- Automatic cleanup of threads.
- No explicit shutdown needed.
- Fails fast if any task fails.
⚡ Handling Failures Gracefully
The previous implementation fails entirely if any of the tasks fail. Instead, let’s ensure that we use available data and provide default values for failures.
🔧 Improved Implementation: Handling Failures Gracefully
@Service
public class FlightService {
public List<Integer> fetchFlightPrices() throws InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<>()) {
var price1 = scope.fork(() -> fetchPrice("Airline1"));
var price2 = scope.fork(() -> fetchPrice("Airline2"));
var price3 = scope.fork(() -> fetchPrice("Airline3"));
scope.join(); // Wait for all tasks
return List.of(
getOrDefault(price1, 4000),
getOrDefault(price2, 5000),
getOrDefault(price3, 4500)
);
}
}
private int fetchPrice(String airline) {
try {
if ("Airline2".equals(airline)) throw new RuntimeException("Service Down"); // Simulate failure
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
return new Random().nextInt(5000);
}
private int getOrDefault(StructuredTaskScope.Subtask<Integer> task, int defaultValue) {
try {
return task.get(); // Try getting result
} catch (Exception e) {
return defaultValue; // ✅ Return default if failed
}
}
}
🔹 Controller to Expose API
@RestController
@RequestMapping("/flights")
public class FlightController {
@Autowired
private FlightService flightService;
@GetMapping("/prices")
public List<Integer> getFlightPrices() throws InterruptedException {
return flightService.fetchFlightPrices();
}
}
✅ Key Enhancements:
- 🎯 Graceful Failure Handling: Uses
getOrDefault()
to return a default price instead of failing. - 🚀 Uses
ShutdownOnSuccess
Scope: Doesn’t cancel all tasks if one fails. - ⚡ Ensures All Available Prices Are Used: Instead of failing completely, it returns defaults for failed tasks.
📌 Conclusion: When to Use Structured Concurrency?
✅ Use Structured Concurrency If:
- You want better control over concurrent tasks.
- You need automatic cancellation and structured error handling.
- You’re working with Spring Boot services that make multiple parallel API calls.
❌ Don’t Use If:
- You need non-blocking reactive streams → Use Spring WebFlux (Project Reactor) instead.
- Your app is on Java <21 → Structured Concurrency is only available in Java 21+ (preview feature).