Back to CompletableFuture & Resilience
Q8Question 8 of 19

When does CompletableFuture improve performance?

Key Concepts
  • CF improves performance when independent IO-bound tasks run in parallel: fan-out then combine.
  • CF adds no value for sequential dependent operations where each step needs the previous result.
  • CF is pointless for supplyAsync().get() called immediately: same as a direct call with extra overhead.
  • CF hurts for CPU-bound work on small data: parallel overhead exceeds computation time.
  • CF adds value when the caller can do other work while the pipeline runs (true non-blocking).
  • Decision: identify independent calls that can run in parallel and use allOf() to wait for all.

Definition

CompletableFuture improves performance when independent IO-bound tasks run in parallel (fan-out/fan-in), but adds overhead without benefit for sequential dependencies, immediate blocking get(), and trivial operations.
  • 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.

JAVA
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

JAVA
// 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()

JAVA
// 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

JAVA
// 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.

DIAGRAM
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?

JAVA
// 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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate