Question 2 Β· Section 19

What are the main advantages of CompletableFuture over Future

CompletableFuture has 5 main advantages over regular Future:

Language versions: English Russian Ukrainian

🟒 Junior Level

CompletableFuture has 5 main advantages over regular Future:

  1. Non-blocking β€” can build chains without waiting
  2. Combining β€” can merge multiple CompletableFutures
  3. Error handling β€” built-in methods for exceptions
  4. Manual completion β€” can complete the result at any time
  5. More methods β€” 40+ methods for different scenarios
// Future β€” can only wait
Future<String> future = executor.submit(() -> "Hello");
String result = future.get();  // blocks!

// CompletableFuture β€” chain without waiting
CompletableFuture.supplyAsync(() -> "Hello")
    .thenApply(s -> s + " World")
    .thenAccept(System.out::println);  // non-blocking!

🟑 Middle Level

Detailed advantages

1. Composability:

// Future β€” cannot combine
Future<Integer> f1 = executor.submit(() -> 10);
Future<Integer> f2 = executor.submit(() -> 20);
// Need to wait for both and add manually

// CompletableFuture β€” thenCombine
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> 20);

cf1.thenCombine(cf2, (a, b) -> a + b)
   .thenAccept(System.out::println);  // 30

2. Error handling:

// Future β€” manual try/catch
try {
    Integer result = future.get();
} catch (ExecutionException e) {
    // manual handling
}

// CompletableFuture β€” exceptionally
cf.exceptionally(ex -> {
    log.error("Error", ex);
    return defaultValue;
});

3. Manual completion:

// Can complete manually
CompletableFuture<String> cf = new CompletableFuture<>();

// In another thread:
cf.complete("result");     // set result
cf.completeExceptionally(new RuntimeException("Error"));  // set error

4. Async chain without blocking:

// Future β€” blocks at every step
String url = future1.get();
String data = future2.get();
Result result = process(url, data);

// CompletableFuture β€” entire chain is async
supplyAsync(() -> getUrl())
    .thenCompose(url -> fetchDataAsync(url))
    .thenApply(data -> processData(data))
    .thenAccept(result -> save(result));

5. Timeout support:

// Java 9+
cf.orTimeout(5, TimeUnit.SECONDS);
cf.completeOnTimeout(defaultValue, 5, TimeUnit.SECONDS);

Typical mistakes

  1. Not using async methods: ```java // ❌ Executes in the same thread cf.thenApply(s -> s.toUpperCase());

// βœ… Executes asynchronously cf.thenApplyAsync(s -> s.toUpperCase(), executor);

// thenApply in the same thread is NOT a bug, it's intentional design.
// Use it for lightweight transformations. For blocking β€” only *Async.

---

## πŸ”΄ Senior Level

### Internal Implementation

**CompletionStage interface:**
```java
// CompletableFuture implements CompletionStage<T>
// CompletionStage provides 40+ methods:
// - thenApply, thenAccept, thenRun
// - thenCombine, thenCompose
// - allOf, anyOf
// - exceptionally, handle, whenComplete

// Each method returns a CompletionStage β€” can build chains

Non-blocking execution:

// CompletableFuture uses a callback-based approach
// Instead of blocking get() β€” callback runs upon completion

class CompletableFuture<T> {
    volatile Object result;
    volatile Completion stack;  // linked list of callbacks

    // Upon completion β€” all callbacks execute
    void completeValue(T t) {
        // CAS for setting result
        // Then all pending callbacks execute
    }
}

Architectural Trade-offs

Approach Pros Cons
CompletableFuture Non-blocking, composable Harder to debug
Future + get() Simple Blocking
Reactive Streams Backpressure, streaming More complex
Virtual Threads Simple code, non-blocking Java 21+

Edge Cases

1. Exception wrapping:

// Exceptions are wrapped in CompletionException
cf.thenApply(s -> {
    throw new RuntimeException("Original");
}).exceptionally(ex -> {
    // ex β€” CompletionException
    // ex.getCause() β€” original exception
    return null;
});

2. Cancellation:

// CompletableFuture.cancel() does not interrupt execution
cf.cancel(true);  // mayInterruptIfRunning is ignored

// Need to handle manually
cf.obtrudeValue(null);  // force completion

Performance

// Order of magnitude (depends on JVM and workload):
// Callback: nanoseconds, Async: microseconds, Blocking: milliseconds+

Production Experience

Microservice communication:

@Service
public class OrderService {
    private final UserService userService;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public CompletableFuture<Order> createOrder(CreateOrderRequest req) {
        return userService.validateUserAsync(req.userId())
            .thenCompose(user ->
                inventoryService.checkStockAsync(req.items())
                    .thenCompose(stock ->
                        paymentService.processPaymentAsync(req.payment())
                            .thenApply(payment ->
                                new Order(user, stock, payment)
                            )
                    )
            )
            .exceptionally(ex -> {
                log.error("Order creation failed", ex);
                throw new OrderCreationException(ex);
            });
    }
}

Best Practices

// βœ… Chains instead of blocking
cf.thenApply(...).thenAccept(...);

// βœ… Error handling
cf.exceptionally(ex -> defaultValue);

// βœ… Custom Executor for I/O
CompletableFuture.supplyAsync(task, ioExecutor);

// βœ… Combining
cf1.thenCombine(cf2, (a, b) -> combine(a, b));

// ❌ Blocking get() without reason
// ❌ Ignoring exceptions
// ❌ commonPool for I/O operations

When NOT to use CompletableFuture

  • Simple fire-and-forget β€” executor.submit() is enough
  • Virtual Threads (Java 21+) β€” often simpler to write synchronous code
  • Data streams β€” reactive (Flux/Mono) is better

🎯 Interview Cheat Sheet

Must know:

  • Non-blocking composition β€” chains without blocking get()
  • Combining β€” thenCombine, allOf, anyOf for parallel calls
  • Error handling β€” exceptionally, handle, whenComplete
  • Manual completion β€” complete(), completeExceptionally()
  • 40+ CompletionStage methods for any scenario
  • Timeout support β€” orTimeout(), completeOnTimeout() (Java 9+)

Frequent follow-up questions:

  • Main advantage over Future? β€” Non-blocking composition and error handling without get()
  • How to combine two CFs? β€” thenCombine(cf2, combiner) for parallel, thenCompose for dependent
  • When NOT to use? β€” Simple fire-and-forget, Virtual Threads (Java 21+), data streams (Reactor is better)
  • How to avoid blocking? β€” thenApply/thenAccept chains instead of get()/join()

Red flags (DO NOT say):

  • β€œFuture and CompletableFuture are interchangeable” β€” Future has no chains or error handling
  • β€œthenCompose and thenCombine are the same” β€” thenCompose is sequential, thenCombine is parallel
  • β€œI always use commonPool” β€” for I/O this is thread pool starvation

Related topics:

  • [[1. What is CompletableFuture and how does it differ from Future]]
  • [[4. What is the difference between thenApply() and thenCompose()]]
  • [[8. How to combine results of multiple CompletableFutures]]
  • [[24. When to use CompletableFuture vs reactive programming]]