What is CompletableFuture and how does it differ from Future
The main difference from a regular Future:
🟢 Junior Level
CompletableFuture is a class in Java (introduced in Java 8) that represents the result of an asynchronous operation that is not yet ready but will be in the future.
The main difference from a regular Future:
Future— only waiting for the result (get()blocks the thread)CompletableFuture— you can build chains of actions, combine them, handle errors
// Future — need to wait and block the thread
Future<String> future = executor.submit(() -> "Hello");
String result = future.get(); // blocks the thread!
// CompletableFuture — can build a chain
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");
cf.thenApply(s -> s + " World")
.thenAccept(System.out::println); // non-blocking!
Simple analogy:
Future— like a receipt at a restaurant: you wait for the food to be readyCompletableFuture— like delivery: you ordered, and they will bring it to you while you do your own things
🟡 Middle Level
How it works
Future limitations:
// ❌ Future — cannot build a chain
Future<Integer> future = executor.submit(() -> 42);
// Need to block and handle manually
try {
Integer result = future.get(); // blocks
Integer doubled = result * 2;
} catch (InterruptedException | ExecutionException e) {
// manual handling
}
CompletableFuture capabilities:
// ✅ Chain of actions
CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> 42);
cf.thenApply(result -> result * 2) // transformation
.thenAccept(doubled -> System.out.println(doubled)) // consumption
.exceptionally(ex -> { // error handling
System.err.println("Error: " + ex);
return null;
});
// Does not block the main thread!
Creation:
// supplyAsync — with return value
CompletableFuture<String> cf1 =
CompletableFuture.supplyAsync(() -> "Hello");
// runAsync — no return value
CompletableFuture<Void> cf2 =
CompletableFuture.runAsync(() -> System.out.println("Done"));
Typical mistakes
- Forgot Async — executes in the same thread: ```java CompletableFuture.supplyAsync(() -> “Hello”) .thenApply(s -> { // Executes in ForkJoinPool.commonPool() return s + “ World”; }); // This is NOT a mistake, it’s a deliberate choice. thenApply in the same thread — // the right choice for lightweight CPU transformations. The problem only arises if // thenApply performs a blocking operation.
// vs
CompletableFuture
2. **Unhandled exceptions:**
```java
// ❌ Exception is lost
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Error");
}).thenApply(s -> s.toUpperCase());
// ✅ Handling
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Error");
}).exceptionally(ex -> "default");
🔴 Senior Level
Internal Implementation
CompletableFuture vs Future:
// Future — interface
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws ...;
}
// CompletableFuture — implementation + CompletionStage
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
// CompletionStage provides 40+ methods for composition
}
Internal structure:
// CompletableFuture uses lock-free algorithms
// CAS (Compare-And-Swap) for completion
// Stack of completions/dependencies for chains
class CompletableFuture<T> {
volatile Object result; // either value or AltResult (exception)
// AltResult — internal JDK wrapper class for exceptions.
// Allows storing null as a valid result and distinguishing it from an exception.
volatile Completion stack; // dependency stack
// CAS for completion
boolean completeValue(T t) {
// compareAndSwap for setting result
}
}
Architectural Trade-offs
| Future | CompletableFuture |
|---|---|
| Blocking get() | Non-blocking |
| No error handling | Full error handling |
| Cannot combine | thenCombine, allOf, anyOf |
| Needs ExecutorService | Own ForkJoinPool by default |
Edge Cases
1. Default Executor:
// CompletableFuture uses ForkJoinPool.commonPool()
// For I/O operations — bad (too few threads)
CompletableFuture.supplyAsync(() -> {
// I/O operation — need more threads!
return httpClient.get(url);
});
// ✅ Custom Executor
ExecutorService ioExecutor = Executors.newFixedThreadPool(20);
CompletableFuture.supplyAsync(() -> httpClient.get(url), ioExecutor);
2. Exception propagation:
// Exception is wrapped in CompletionException
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Original");
}).thenApply(s -> s.toUpperCase())
.exceptionally(ex -> {
// ex — CompletionException with the original cause
Throwable cause = ex.getCause(); // "Original"
return null;
});
Performance
Operation | Future | CompletableFuture
----------------------|--------|------------------
Creation | 5 ns | 10 ns
get() (blocking) | 100μs+ | 100μs+
thenApply (chain) | N/A | 5-10 ns
Error handling | Manual | Built-in
ForkJoinPool.commonPool():
- Size = availableProcessors - 1
- For CPU-bound tasks — OK
- For I/O — need your own Executor
Production Experience
Async API call:
@Service
public class UserService {
private final RestTemplate restTemplate;
private final ExecutorService executor;
public CompletableFuture<User> getUserAsync(Long userId) {
return CompletableFuture.supplyAsync(() -> {
return restTemplate.getForObject(
"http://api/users/" + userId, User.class
);
}, executor);
}
// Combining multiple calls
public CompletableFuture<UserProfile> getProfile(Long userId) {
CompletableFuture<User> user = getUserAsync(userId);
CompletableFuture<Order> lastOrder = getLastOrderAsync(userId);
return user.thenCombine(lastOrder, (u, o) ->
new UserProfile(u, o)
);
}
}
Best Practices
// ✅ Always handle exceptions
cf.exceptionally(ex -> defaultValue);
// ✅ Use your own Executor for I/O
CompletableFuture.supplyAsync(task, ioExecutor);
// ✅ Don't block without necessity
// ❌ cf.get() — only if truly needed
// ❌ Don't ignore CompletableFuture
// ❌ Don't use commonPool for I/O
🎯 Interview Cheat Sheet
Must know:
- CompletableFuture appeared in Java 8, implements Future + CompletionStage
- CompletionStage provides 40+ methods for composition
- ForkJoinPool.commonPool() is used by default (availableProcessors - 1)
- supplyAsync() — with return value, runAsync() — without
- Exceptions are wrapped in CompletionException
- thenApply/thenAccept/thenRun — for chaining, thenCombine/allOf/anyOf — for combining
Frequent follow-up questions:
- How does it differ from Future? — Future only has blocking get(), CompletableFuture — non-blocking with chains and error handling
- What pool is used by default? — ForkJoinPool.commonPool(), for I/O need your own Executor
- How to handle errors? — exceptionally(), handle(), whenComplete()
- Can you complete manually? — Yes, complete() or completeExceptionally()
Red flags (DO NOT say):
- “CompletableFuture blocks the thread” — it is non-blocking by design
- “I use commonPool for HTTP requests” — thread pool starvation
- “I ignore CompletableFuture without handling” — lost exception
Related topics:
- [[2. What are the main advantages of CompletableFuture over Future]]
- [[12. What thread pool is used by default for async methods]]
- [[6. How to handle exceptions in CompletableFuture chain]]
- [[16. What does supplyAsync() method do and when to use it]]