Can you reuse a single CompletableFuture in multiple chains
Store the CompletableFuture in a cache instead of the actual value:
π’ Junior Level
Yes, you can! One CompletableFuture can be used in multiple chains. This is called fan-out β one result, many consumers.
CompletableFuture<User> userCf = userService.fetchAsync(id);
// Three independent chains, one shared CF
userCf.thenAccept(user -> loadOrders(user));
userCf.thenAccept(user -> checkBalance(user));
userCf.thenRun(() -> updateLastLogin());
Key points:
- The result is computed exactly once
- All subscribers receive the same object
- If the CF is already completed β the new callback executes immediately
π‘ Middle Level
Promise Caching pattern
Store the CompletableFuture in a cache instead of the actual value:
private final Map<Long, CompletableFuture<User>> cache = new ConcurrentHashMap<>();
public CompletableFuture<User> getUserAsync(Long id) {
return cache.computeIfAbsent(id, userId ->
userService.fetchAsync(userId)
.whenComplete((result, ex) -> {
// Remove from cache after completion (success or error)
cache.remove(userId);
})
);
}
Advantage: If 10 threads request the same data simultaneously, they all subscribe to one CF. Only the first one initiates the DB query, the rest wait for the result.
Danger: mutating shared result
All chains receive a reference to the same object.
CompletableFuture<User> userCf = userService.fetchAsync(id);
// Chain 1 β mutates the object!
userCf.thenAccept(user -> user.setName("Modified"));
// Chain 2 β sees the modified data!
userCf.thenAccept(user -> System.out.println(user.getName())); // "Modified"
Rule: The result passed to multiple chains must be immutable.
Callback execution order
The JDK does not guarantee the order of callback execution. In the current implementation (JDK 8-21)
the order depends on internal details. If order matters β explicitly enforce it via
thenCompose or thenCombine.
// β Order is not guaranteed
cf.thenAccept(a -> step1(a));
cf.thenAccept(a -> step2(a)); // not necessarily after step1
// β
Explicit order
cf.thenCompose(a -> step1Async(a).thenAccept(r -> step2(r)));
π΄ Senior Level
Memory Leak mechanism
Each dependent CF (result of thenApply, thenAccept, etc.) holds a reference
to its upstream. Until the dependent chain completes and there are references to it,
the GC cannot collect the entire graph. In long-running applications this can lead
to a leak if you create endless chains without completion.
// Problem: endless chain buildup
CompletableFuture<Void> cf = CompletableFuture.completedFuture(null);
for (int i = 0; i < 1_000_000; i++) {
cf = cf.thenAccept(v -> doSomething());
// Each new CF holds a reference to the previous one
// Until the last one lives β GC cannot collect the whole graph
}
Cancellation propagation
If you cancel the root CF, all dependent chains complete with CancellationException. Cancelling a child chain does not affect the root CF.
CompletableFuture<String> root = CompletableFuture.supplyAsync(() -> "data");
CompletableFuture<String> child1 = root.thenApply(s -> s + " -> 1");
CompletableFuture<String> child2 = root.thenApply(s -> s + " -> 2");
child1.cancel(false);
// root and child2 continue working
Architectural pattern: Request Collapsing
In highload systems, one CF is used to prevent duplicate requests:
public CompletableFuture<Data> getData(String key) {
return inFlight.computeIfAbsent(key, k ->
loadDataFromDb(k)
.whenComplete((v, ex) -> inFlight.remove(k))
);
}
// 100 simultaneous requests for the same key β 1 actual DB query
Best Practices
// β
One CF β many subscribers (fan-out)
// β
Immutable result
// β
Cache cleanup after completion (whenComplete)
// β
Explicit order via thenCompose/thenCombine
// β Mutating shared result
// β Endless chains without completion
// β Depending on callback order
// β Forgotten references in cache to completed CFs
π― Interview Cheat Sheet
Must know:
- One CF can be used in multiple chains β fan-out pattern
- Result is computed exactly once, all subscribers receive the same object
- Promise Caching: Map<Key, CompletableFuture> to prevent duplicate requests
- Request Collapsing: 100 requests for the same key β 1 actual DB query
- Callback order is NOT guaranteed by the JDK
Common follow-up questions:
- Is callback order guaranteed? β No. If critical β enforce explicitly via thenCompose/thenCombine
- How does memory leak occur? β Endless chains: each CF holds a reference to upstream, GC cannot collect
- Does cancelling a child chain affect the root? β No. Cancelling the root β affects all children
- Is mutating shared result a problem? β Yes, all chains see the same object. Result must be immutable
Red flags (DO NOT say):
- βEach subscriber gets a copy of the resultβ β all receive the same object
- βthenAccept order is guaranteedβ β JDK does not guarantee order
- βNo need for CF cache β CF cleans itself upβ β completed CFs need to be removed from cache
Related topics:
- [[16. How to properly execute multiple parallel requests to microservices]]
- [[8. How to combine results of multiple CompletableFuture]]
- [[18. How to cancel CompletableFuture execution]]
- [[24. How to test code with CompletableFuture]]