Stop Slamming Downstream Services: Singleflight Request Coalescing with Java Virtual Threads
Virtual threads solved your JVM I/O bottlenecks, but downstream internal services are now on fire because 50,000 concurrent threads are fetching the exact same cache miss simultaneously. You do not need distributed locks or larger database instances; you need the Singleflight request coalescing pattern inside your JVM.
Why Most Developers Get This Wrong
- Reaching for distributed locking: Slapping Redisson or Redis locks over hot cache misses adds unnecessary network hops and operational latency for what is fundamentally an in-process duplicate query problem.
-
Blocking with synchronized blocks: Using coarse
ReentrantLockorsynchronizedguards pinned carrier threads in early Loom setups and destroys virtual thread scalability across high-throughput services. - Relying purely on TTL caches: When a hot Redis or Caffeine key expires under 20k RPS, raw uncoordinated reads trigger immediate thundering herds across downstream gRPC/REST endpoints.
The Right Way
Suppress duplicate concurrent calls locally by routing identical in-flight keys to a single downstream execution using lock-free coordination.
- Use a
ConcurrentHashMapto register in-flightCompletableFutureinstances keyed by query identity. - The first virtual thread registers the promise and triggers the downstream RPC, while subsequent threads simply wait on the shared future.
- Leverage
CompletableFuture.join()—virtual threads unmount cleanly from OS carrier threads while awaiting the result. - Automatically evict keys in a
finallyblock or completion callback so future requests trigger fresh executions.
Show Me The Code
Here is an idiomatic, lock-free Singleflight implementation using standard Java concurrency utilities:
public class Singleflight<K, V> {
private final ConcurrentHashMap<K, CompletableFuture<V>> inFlight = new ConcurrentHashMap<>();
public V execute(K key, Supplier<V> task) {
return inFlight.computeIfAbsent(key, k -> {
var future = new CompletableFuture<V>();
Thread.ofVirtual().start(() -> {
try { future.complete(task.get()); }
catch (Throwable ex) { future.completeExceptionally(ex); }
finally { inFlight.remove(k); }
});
return future;
}).join();
}
}
Key Takeaways
- Protect your downstream: Virtual threads can absorb massive ingress spikes; without in-JVM coalescing, that traffic acts as a self-inflicted DDoS against internal dependencies.
-
Lock-free coordination wins: Combining
ConcurrentHashMap.computeIfAbsent()withCompletableFutureeliminates thread contention while keeping memory overhead negligible. -
Clean up immediately: Always evict keys in
finallyblocks to avoid memory leaks and prevent transient downstream exceptions from permanently poisoning future callers.
I built javalld.com while prepping for senior roles — complete LLD problems with execution traces, not just theory.
Top comments (0)