Question 20 Β· Section 19

Can you reuse a single CompletableFuture in multiple chains

Store the CompletableFuture in a cache instead of the actual value:

Language versions: English Russian Ukrainian

🟒 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]]