Definition
- CF helps: parallel IO – independent tasks run simultaneously; latency = max(individual times) instead of sum.
- CF helps: non-blocking – caller does other work while the pipeline runs; true async benefit.
- CF hurts: sequential dependencies – step B needs step A's result; no parallelism possible; CF adds thread-switch overhead.
- CF pointless: immediate get() –
supplyAsync(task).get()blocks the caller anyway; same as calling task directly with extra overhead. - CF hurts: CPU-bound small data – parallel overhead (thread creation, scheduling) exceeds computation time.
- Decision – check if calls are independent; if yes, fan out with allOf(); if no, use direct calls.
At National Bank of India: CF helps when you can fetch Rajesh's balance, Priya's balance, and the exchange rate all three simultaneously instead of one after another. Three independent 200ms calls take 200ms parallel instead of 600ms sequential. CF hurts when each step depends on the previous: fetch account ID, then fetch balance with that ID, then calculate interest on that balance. That is sequential no matter what.
Understanding the Concepts
When does CompletableFuture improve performance?
CF improves performance when you have independent IO-bound tasks that can run in parallel.
package com.bank.service;
import java.util.concurrent.*;
import java.math.BigDecimal;
public class ParallelBenefit {
public static void main(String[] args) {
ExecutorService ioPool = Executors.newFixedThreadPool(10);
// Sequential: 200 + 300 + 150 = 650ms
// Parallel: max(200, 300, 150) = 300ms
CompletableFuture<BigDecimal> balanceCF = CompletableFuture
.supplyAsync(() -> fetchBalance(), ioPool); // 200ms
CompletableFuture<Integer> scoreCF = CompletableFuture
.supplyAsync(() -> fetchCreditScore(), ioPool); // 300ms
CompletableFuture<BigDecimal> propertyCF = CompletableFuture
.supplyAsync(() -> fetchPropertyValue(), ioPool); // 150ms
CompletableFuture.allOf(balanceCF, scoreCF, propertyCF).join();
// All done in ~300ms instead of 650ms!
ioPool.shutdown();
}
static BigDecimal fetchBalance() {
sleep(200); return new BigDecimal("500000");
}
static int fetchCreditScore() {
sleep(300); return 750;
}
static BigDecimal fetchPropertyValue() {
sleep(150); return new BigDecimal("3500000");
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
When does CompletableFuture NOT help?
Sequential dependencies: no parallelism possible
// Each step needs the previous result. Still sequential!
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> getAccountId("Rajesh")) // 100ms
.thenCompose(id -> fetchBalanceAsync(id)) // 200ms, needs id
.thenApply(balance -> calculateInterest(balance)) // 50ms, needs balance
.thenApply(interest -> formatReport(interest)); // 10ms, needs interest
// Total: 100 + 200 + 50 + 10 = 360ms
// Direct calls: also 360ms, but simpler and no CF overhead
Immediate blocking get()
// POINTLESS: blocks immediately
String data = CompletableFuture.supplyAsync(() -> dbQuery()).get();
// Equivalent to (but slower than):
String data = dbQuery();
// The .get() blocks the caller. No parallelism. Extra overhead.
CPU-bound on small data
// For small lists, parallel overhead exceeds computation
long sum = smallList.parallelStream().mapToLong(this::compute).sum();
// Parallel overhead: thread scheduling, splitting, merging
// If compute() takes < 0.1ms per item and list has < 1000 items,
// sequential is faster!
What is the decision guide?
| Scenario | CF Benefit | Recommendation |
|---|---|---|
| 3 independent HTTP calls | 3x speedup | Use CF with allOf |
| Fan-out to 10 services | 10x speedup | Use CF with allOf |
| Non-blocking pipeline | Caller freed | Use CF pipeline |
| Sequential A then B then C | No speedup | Direct calls |
| supplyAsync().get() | No benefit | Direct call |
| CPU-bound small data | Negative | Sequential loop |
| Trivial 1ms operation | Negative | Direct call |
How do you identify parallelizable calls?
Draw a dependency graph. Independent nodes (no edges between them) can run in parallel. Dependent chains must be sequential.
Loan Approval: fetchCreditScore ──────┐ fetchPropertyValue ─────┼──▶ decide() fetchIncomeProof ───────┘ All three are independent: use CF.allOf() Latency: max(score, property, income) instead of sum
What are common mistakes?
// WRONG: wrapping every call in CF "just because"
CompletableFuture.supplyAsync(() -> simpleCalculation()).join();
// Overhead > benefit for a 1ms calculation
// WRONG: sequential calls forced into CF pipeline
cf.thenCompose(a -> fetchB(a)).thenCompose(b -> fetchC(b));
// Still sequential! Simpler as: B b = fetchB(a); C c = fetchC(b);
// CORRECT: identify independence, use allOf()
CF<A> a = supplyAsync(() -> fetchA(), pool);
CF<B> b = supplyAsync(() -> fetchB(), pool);
CF.allOf(a, b).join();
// True parallel benefit!
What are common interview follow-up questions?
- Can CF make sequential code faster? Only if the caller can do other work while the pipeline runs (true non-blocking pattern).
- When is the overhead of CF noticeable? For operations under 1ms, the thread switch and object creation overhead is significant.
- What is the minimum latency savings to justify CF? Generally, when parallel calls save at least 50ms compared to sequential.
Visual Overview
Independent calls: CF HELPS
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Balance │ │ Score │ │ Property │
│ 200ms │ │ 300ms │ │ 150ms │
└─────┬────┘ └─────┬────┘ └─────┬────┘
│ │ │
└─────────────┼─────────────┘
▼
allOf().join()
Total: 300ms (not 650ms)
Sequential dependencies: CF ADDS OVERHEAD
getAccountId ──▶ fetchBalance ──▶ calcInterest ──▶ format
100ms 200ms 50ms 10ms
Total: 360ms (same with or without CF)
Key Takeaway
CompletableFuture improves performance when independent IO-bound tasks run in parallel. Three independent 200ms calls take 200ms with allOf() instead of 600ms sequential. That is the primary use case: fan-out to multiple services, wait for all, combine results.
CF adds no value when operations are sequentially dependent (each step needs the previous result), when you call get() immediately after supplyAsync() (the caller blocks anyway), or for trivial operations where the thread-switch overhead exceeds the computation time.
The decision process is straightforward: draw the dependency graph, identify independent calls, fan them out with allOf(). At National Bank of India, loan approval with three independent checks runs in parallel: 300ms instead of 650ms. The account-to-balance-to-interest pipeline is sequential and uses direct calls: simpler, faster, no CF overhead.
Remember one sentence: CF shines when independent IO calls run in parallel; it adds overhead without benefit for sequential dependencies.
---