java 144 lines · 3 tabs

Single-Flight Cache Loader: Coalesce Concurrent Identical Loads in Java

Shared by codesnips Aug 2026
3 tabs
package com.example.cache;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

public class SingleFlightLoader<K, V> {

    private final ConcurrentHashMap<K, CompletableFuture<V>> inFlight = new ConcurrentHashMap<>();

    public V load(K key, Supplier<V> loader) {
        AtomicBoolean weAreLeader = new AtomicBoolean(false);

        CompletableFuture<V> future = inFlight.computeIfAbsent(key, k -> {
            weAreLeader.set(true);
            return new CompletableFuture<>();
        });

        if (weAreLeader.get()) {
            runLoad(key, future, loader);
        }

        try {
            return future.join();
        } catch (CompletionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            throw e;
        }
    }

    private void runLoad(K key, CompletableFuture<V> future, Supplier<V> loader) {
        try {
            future.complete(loader.get());
        } catch (Throwable t) {
            future.completeExceptionally(t);
        } finally {
            // Only remove if this exact future is still published.
            inFlight.remove(key, future);
        }
    }
}
3 files · java Explain with highlit

When a hot cache key expires under load, many threads can miss simultaneously and all run the same expensive load — hitting the database or an upstream service N times for one value. This is the thundering-herd (cache stampede) problem, and the single-flight pattern solves it by letting exactly one load run per key while every other concurrent caller waits for and shares that single result.

The SingleFlightLoader tab implements the guard. It keeps a ConcurrentHashMap<K, CompletableFuture<V>> of in-flight loads. The core trick is computeIfAbsent: for a given key it either finds an existing future or atomically installs a brand-new incomplete one. computeIfAbsent runs its mapping function under the map bin's lock, so only the winning thread receives true from the AtomicBoolean and becomes the loader; all losers get the same future reference and simply call future.join(). The loader then runs the supplier outside the map lock to avoid holding it during slow I/O, completing the shared future with either the value or the exception.

Cleanup matters: the finally block removes the entry with inFlight.remove(key, future), the two-argument form that only deletes if the value is still the exact future it published. That prevents a race where a newer load for the same key gets clobbered by a stale cleanup. Failures propagate to all waiters through completeExceptionally, so a broken load does not leave callers hung, and the entry is still removed so the next call retries fresh rather than caching the error.

The CachedProductService tab shows the read-through wiring. It checks a Guava Cache first, and only on a miss delegates to the loader, storing the resolved value back into the cache. Because the loader coalesces, a burst of misses for one product id triggers a single repository.findById call. The SingleFlightLoaderTest tab demonstrates the guarantee: 50 threads request the same key against a supplier that increments an AtomicInteger, and the assertion confirms the underlying load ran exactly once. This approach shines for expensive, idempotent reads with high key contention; the trade-off is that a slow load makes all waiters wait, so pairing it with a timeout on join or a stale-while-revalidate policy is wise for latency-sensitive paths.


Related snips

Share this code

Here's the card — post it anywhere.

Single-Flight Cache Loader: Coalesce Concurrent Identical Loads in Java — share card
Link copied