Java has been around for nearly three decades, and we’ve had threads since day one. We got java.util.concurrent
in 2004, lambdas in 2014, CompletableFuture improvements, reactive streams, and virtual threads in 2023…
And yet, writing correct concurrent code in Java still feels like navigating a minefield.
Why is this still so hard?
The Problem: Too Many Half-Solutions
Let’s take a simple case:
Fetch data from three APIs concurrently, process results, handle errors, and respect a timeout.
1. Threads (1995)
public List<String> fetchData() {
List<String> results = Collections.synchronizedList(new ArrayList<>());
CountDownLatch latch = new CountDownLatch(3);
Thread t1 = new Thread(() -> {
try {
results.add(callApi("api1"));
} catch (Exception e) {
// What do we do here?
} finally {
latch.countDown();
}
});
// … repeat for t2, t3 …
t1.start(); t2.start(); t3.start();
try {
latch.await(10, TimeUnit.SECONDS); // What if it times out?
} catch (InterruptedException e) {
// Now what? Cancel the threads?
}
return results; // Hope for the best
}
Problems:
Manual lifecycle management
No consistent error propagation
Cancellation is basically impossible
2. ExecutorService (2004)
java
Copy code
public List<String> fetchData() throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
try {
List<Future<String>> futures = List.of(
executor.submit(() -> callApi("api1")),
executor.submit(() -> callApi("api2")),
executor.submit(() -> callApi("api3"))
);
List<String> results = new ArrayList<>();
for (Future<String> f : futures) {
try {
results.add(f.get(10, TimeUnit.SECONDS));
} catch (TimeoutException e) {
f.cancel(true);
}
}
return results;
} finally {
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
executor.shutdownNow(); // Fingers crossed
}
}
Better, but still verbose and error-prone.
3. CompletableFuture (2014)
java
Copy code
public CompletableFuture<List<String>> fetchData() {
var f1 = CompletableFuture.supplyAsync(() -> callApi("api1"));
var f2 = CompletableFuture.supplyAsync(() -> callApi("api2"));
var f3 = CompletableFuture.supplyAsync(() -> callApi("api3"));
return CompletableFuture.allOf(f1, f2, f3)
.thenApply(v -> List.of(f1.join(), f2.join(), f3.join()))
.orTimeout(10, TimeUnit.SECONDS)
.exceptionally(ex -> List.of());
}
Cleaner, but:
No structured concurrency
Cancellation is awkward
Error handling is ad-hoc
4. Virtual Threads (2023)
java
Copy code
public List<String> fetchData() throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = List.of(
executor.submit(() -> callApi("api1")),
executor.submit(() -> callApi("api2")),
executor.submit(() -> callApi("api3"))
);
return futures.stream()
.map(f -> {
try {
return f.get(10, TimeUnit.SECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toList();
}
}
Virtual threads help performance, but the core problems remain:
No automatic cancellation
No clear error boundaries
Timeouts are still manual
# What’s Wrong Here?
After 20+ years, Java’s concurrency is still missing:
Structured Concurrency — no parent/child lifetimes, leading to leaks.
Reliable Cancellation — interruption is unreliable and inconsistent.
Consistent Error Handling — failures don’t cleanly propagate.
Resource Safety — Executors and threads must be closed manually.
Context — no standard way to pass cancellation tokens, timeouts, or tracing.
Other languages got this right:
Go has context.WithTimeout for group cancellation.
Kotlin has coroutines with structured scopes.
C# has Tasks with CancellationToken.
# What We Actually Want
try (var scope = new CoroutineScope()) {
var results = List.of("api1", "api2", "api3").stream()
.map(api -> scope.async(suspend -> callApi(suspend, api)))
.map(handle -> handle.join())
.toList();
return results;
} // Automatic cleanup, cancellation, and error propagation
This is:
Structured — parent/child relationships are explicit
Cancellable — cooperative and consistent
Safe — resources cleaned up automatically
Transparent — errors bubble naturally
# Enter JCoroutines 🚀
Concurrency should feel like an elevator: press a few clear buttons and trust the machinery.
That’s what I set out to build with JCoroutines:
Structured concurrency by default
Explicit context passing (cancellation, timeouts, schedulers)
No compiler magic — just clean Java APIs on top of virtual threads
It’s small, explicit, and available today.
Try It Out
On Maven Central:
maven xml
<dependency>
<groupId>tech.robd</groupId>
<artifactId>jcoroutines</artifactId>
<version>0.1.0</version>
</dependency>
Or Gradle:
kotlin gradle.build.kts
implementation("tech.robd:jcoroutines:0.1.0")
The Path Forward
Java itself is heading this way (see JEP 428 on structured concurrency), but it will take years before that’s fully stable.
Meanwhile, JCoroutines gives you these patterns now — using just Java 21+ and virtual threads.
[📦 Maven Central](https://central.sonatype.com/artifact/tech.robd/jcoroutines)
[💻 GitHub repo](https://github.com/robdeas/jcoroutines)
Top comments (0)