How to Create a CompletableFuture That is Already Completed with Result
In asynchronous programming, situations frequently arise where the result of a computation is already known in advance. Examples include reading from an in-memory cache, providi...
🟢 Junior Level
In asynchronous programming, situations frequently arise where the result of a computation is already known in advance. Examples include reading from an in-memory cache, providing a static default fallback, or returning predetermined data in unit tests. Rather than breaking the signature of a method returning CompletableFuture<T>, specialized factory methods provide immediate instances without spinning up background threads.
Primary Factory Methods
CompletableFuture.completedFuture(T value)(Java 8+): Returns a newCompletableFuturethat is already completed with the given value.CompletableFuture<String> cachedFuture = CompletableFuture.completedFuture("Data from cache"); // The result is immediately available without waiting or thread pool allocation: String result = cachedFuture.join(); // Returns instantlyCompletableFuture.failedFuture(Throwable ex)(Java 9+): Returns an instance that is already completed exceptionally with the provided error.CompletableFuture<User> errorFuture = CompletableFuture.failedFuture( new UserNotFoundException("User not found") );completedStage(T value)andfailedStage(Throwable ex)(Java 12+): Returns a read-onlyCompletionStage<T>interface, protecting the stage against external manual mutation or forced overriding by clients.
🟡 Middle Level
The Conditional Asynchrony Pattern
The most common real-world production pattern for completedFuture is transparent caching. When a cache hit occurs, no background thread or I/O is scheduled:
public CompletableFuture<ProductDetails> getProductDetails(String productId) {
// 1. Check fast local in-memory cache (e.g. Caffeine)
ProductDetails cached = localCache.getIfPresent(productId);
if (cached != null) {
// Instant synchronous return: 0 thread context-switching overhead
return CompletableFuture.completedFuture(cached);
}
// 2. Cache miss: offload slow remote I/O to a dedicated thread pool
return CompletableFuture.supplyAsync(() -> remoteCatalogService.fetch(productId), ioExecutor)
.thenApply(product -> {
localCache.put(productId, product);
return product;
});
}
The Critical Trap: Callback Execution Context
When invoking .thenApply(...) or .thenAccept(...) on an in-flight future, the callback executes on whichever worker thread completes that future.
However, when invoking .thenApply(...) on an already completed future (like one returned by completedFuture):
- The callback executes SYNCHRONOUSLY on the calling thread within the
thenApplymethod call itself.
// DANGEROUS: If heavyCpuCalculation() is slow, it blocks the calling thread (e.g. Tomcat request or UI thread)!
CompletableFuture.completedFuture(userData)
.thenApply(user -> heavyCpuCalculation(user));
// SAFE: When thread isolation is required, always use the *Async variant with an explicit executor
CompletableFuture.completedFuture(userData)
.thenApplyAsync(user -> heavyCpuCalculation(user), computeExecutor);
Java 8 Idiom for Failed Pre-Completed Futures
Java 8 lacked failedFuture(). To create an exceptionally pre-completed future in Java 8, use explicit instantiation:
// Java 8 pattern for failed future:
CompletableFuture<String> failed = new CompletableFuture<>();
failed.completeExceptionally(new IllegalStateException("Validation error"));
🔴 Senior Level
Internal Memory State and Allocation Overhead
When calling CompletableFuture.completedFuture(value):
- A new
CompletableFutureobject is allocated on the heap. - The internal
resultfield is assigned(value == null) ? NIL : value(whereNILis an internal sentinel instance ofAltResult). - The
stackfield (Treiber stack for dependent completion stages) remainsnull. - No threads, work queues, or
ForkJoinPoolworkers are engaged. Method latency is roughly 2–5 nanoseconds.
Highload Optimization: Eliminating Heap Churn for Void
In high-throughput microservices processing hundreds of thousands of operations per second, methods often return CompletableFuture<Void>. Constantly allocating CompletableFuture.completedFuture(null) creates unnecessary allocation churn in the JVM Young Generation.
Zero-Allocation Pattern (as used in Netty and Project Reactor):
public final class AsyncUtils {
// Single shared completed future singleton across the application:
public static final CompletableFuture<Void> COMPLETED_VOID =
CompletableFuture.completedFuture(null);
private AsyncUtils() {}
}
// Usage in highload code paths:
public CompletableFuture<Void> logEventAsync(Event event) {
if (!event.isImportant()) {
return AsyncUtils.COMPLETED_VOID; // Zero heap allocations!
}
return CompletableFuture.runAsync(() -> kafkaProducer.send(event), ioExecutor);
}
Architectural Encapsulation: completedStage (Java 12)
The standard CompletableFuture class allows callers to invoke destructive mutations like cf.complete(val) or cf.obtrudeValue(val). Returning raw CompletableFuture from public service interfaces leaks control to external callers.
Java 12 introduced CompletableFuture.completedStage(value):
public CompletionStage<Session> authenticate(Credentials creds) {
if (creds.isAnonymous()) {
// Returns MinimalStage: a private JDK implementation where complete()
// and obtrude() do not exist or throw UnsupportedOperationException
return CompletableFuture.completedStage(Session.ANONYMOUS);
}
return fetchSessionAsync(creds);
}
4 Tricky Questions
1. What happens if you pass null to CompletableFuture.completedFuture(null)? Does it throw NullPointerException?
Answer:
No, NullPointerException is not thrown.
Inside CompletableFuture, null is safely represented by the static sentinel NIL (an instance of AltResult with a null Throwable). When get() or join() is subsequently called, the implementation unboxes NIL back to null. This is the standard mechanism to create a completed CompletableFuture<Void>.
2. If a method returns CompletableFuture.completedFuture(x) and the caller chains 10 consecutive .thenApply(...) calls without the Async suffix, which thread executes them, and what risk does this introduce in reactive servers (e.g., Netty or Vert.x)?
Answer:
All 10 callbacks execute sequentially and synchronously on the exact thread that called the method and constructed the chain.
If this execution occurs within a non-blocking network I/O thread (such as a Netty EventLoop), and any step in the pipeline performs computationally intensive logic (e.g., JSON serialization, cryptography, or XML parsing), the entire EventLoop thread will be stalled. This causes severe latency spikes for thousands of other client connections multiplexed on that same event loop.
3. How does CompletableFuture.completedStage(T) differ from a simple cast (CompletionStage<T>) CompletableFuture.completedFuture(T)?
Answer:
With a simple interface cast, callers can downcast back to CompletableFuture via (CompletableFuture<T>) stage and invoke mutating methods like complete() or obtrudeValue().
In contrast, completedStage() returns an instance of the private JDK class CompletableFuture.MinimalStage. Attempting to downcast to CompletableFuture produces a ClassCastException. Furthermore, calling stage.toCompletableFuture() creates an independent defensive copy whose mutation will not affect the original stage.
4. What happens when .thenApply(fn) is chained to CompletableFuture.failedFuture(new MyException())? Is fn invoked?
Answer:
The function fn is never invoked.
A future in an exceptional state immediately propagates that failure downstream. All intermediate stages (thenApply, thenAccept, thenCompose) are bypassed until an exception-handling stage (exceptionally, handle, or whenComplete) is encountered in the pipeline.
🎯 Interview Cheat Sheet
Summary in 60 Seconds
- Use
completedFuture(T)for immediate success,failedFuture(Throwable)for immediate failure (Java 9+), andcompletedStage(T)for read-only encapsulation (Java 12+). - Primary application: conditional asynchrony (instant return on cache hit, async I/O on cache miss) and deterministic unit testing stubs.
- Critical Caveat: Callbacks attached via non-async methods (
thenApply,thenAccept) to an already completed future run synchronously on the caller thread. Use*Asyncvariants when offloading compute-intensive work. - In ultra-high-throughput systems, reuse a static
CompletableFuture<Void> COMPLETED_VOIDto eliminate GC allocation churn.
Common Interview Mistakes
- Assuming
completedFuturesubmits tasks to theForkJoinPool(it executes immediately inline in 2-5 ns). - Forgetting that Java 9 introduced
failedFuture(), manually writing verbosenew CompletableFuture()andcompleteExceptionally()boilerplate. - Forgetting that synchronous callbacks on completed futures can inadvertently block sensitive threads (like Netty EventLoops or UI dispatchers).