Definition
- thenApply(Function
) – transforms the result synchronously; likemap()in streams;CFto function returning Integer givesCF. - thenCompose(Function
>) – chains another async operation; likeflatMap()in streams; flattensCFinto> CF. - thenCombine(CF, BiFunction
) – combines two independent futures; both run in parallel, the BiFunction runs when both complete. - Decision rule – function returns plain value: use thenApply; function returns CompletableFuture: use thenCompose; two independent futures: use thenCombine.
At National Bank of India: thenApply is "after you get Rajesh's balance (step 1), calculate the interest (step 2) right there." Step 2 is quick math on the result. thenCompose is "after you get Rajesh's balance (step 1), fetch his credit score from an external bureau (step 2, another async call)." Step 2 is itself an async operation returning a Future. thenCombine is "fetch Rajesh's balance AND Priya's balance in parallel, then calculate the total when both arrive."
Understanding the Concepts
What is thenApply?
thenApply transforms the result of a CompletableFuture synchronously. The function receives the completed value and returns a new value. It is the equivalent of map() in streams.
package com.bank.service;
import java.util.concurrent.CompletableFuture;
import java.math.BigDecimal;
public class ThenApplyDemo {
public static void main(String[] args) {
CompletableFuture<String> balanceStr = CompletableFuture
.supplyAsync(() -> "150000.75");
CompletableFuture<BigDecimal> balance = balanceStr
.thenApply(s -> new BigDecimal(s)); // sync: String to BigDecimal
CompletableFuture<String> formatted = balance
.thenApply(b -> "Balance: Rs." + b); // sync: BigDecimal to String
System.out.println(formatted.join());
}
}
Balance: Rs.150000.75
What is thenCompose?
thenCompose chains another async operation. The function receives the completed value and returns a new CompletableFuture. thenCompose flattens the result so you get CF instead of CF.
package com.bank.service;
import java.util.concurrent.CompletableFuture;
public class ThenComposeDemo {
static CompletableFuture<Integer> fetchCreditScoreAsync(String accountId) {
return CompletableFuture.supplyAsync(() -> {
// Simulate async API call to credit bureau
return 750;
});
}
public static void main(String[] args) {
CompletableFuture<Integer> score = CompletableFuture
.supplyAsync(() -> "SAV2001") // CF<String>
.thenCompose(id -> fetchCreditScoreAsync(id)); // CF<Integer>
// thenCompose flattens: CF<CF<Integer>> becomes CF<Integer>
System.out.println("Credit score: " + score.join());
}
}
Credit score: 750
What is thenCombine?
thenCombine merges two independent futures. Both run in parallel, and the BiFunction runs when both complete.
package com.bank.service;
import java.util.concurrent.CompletableFuture;
import java.math.BigDecimal;
public class ThenCombineDemo {
public static void main(String[] args) {
CompletableFuture<BigDecimal> rajeshBalance = CompletableFuture
.supplyAsync(() -> new BigDecimal("150000"));
CompletableFuture<BigDecimal> priyaBalance = CompletableFuture
.supplyAsync(() -> new BigDecimal("250000"));
CompletableFuture<BigDecimal> total = rajeshBalance
.thenCombine(priyaBalance, BigDecimal::add);
System.out.println("Total: Rs." + total.join());
}
}
Total: Rs.400000
How do thenApply, thenCompose, and thenCombine compare?
| Method | Input | Function returns | Result | Stream equivalent |
|---|---|---|---|---|
| thenApply | CF
| thenCompose | CF
| thenCombine | CF
How do you build a full async pipeline?
package com.bank.service;
import java.util.concurrent.CompletableFuture;
import java.math.BigDecimal;
public class FullPipeline {
static CompletableFuture<BigDecimal> fetchBalanceAsync(String id) {
return CompletableFuture.supplyAsync(() -> new BigDecimal("150000"));
}
static CompletableFuture<BigDecimal> fetchTaxRateAsync() {
return CompletableFuture.supplyAsync(() -> new BigDecimal("0.10"));
}
public static void main(String[] args) {
BigDecimal interestRate = new BigDecimal("0.065");
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> "SAV2001") // get account ID
.thenCompose(id -> fetchBalanceAsync(id)) // async: fetch balance
.thenApply(bal -> bal.multiply(interestRate)) // sync: calculate interest
.thenCombine(fetchTaxRateAsync(), (interest, tax) ->
interest.subtract(interest.multiply(tax))) // combine with tax
.thenApply(net -> "Net interest: Rs." + net) // sync: format
.exceptionally(ex -> "Error: " + ex.getMessage()); // error handling
System.out.println(result.join());
}
}
Net interest: Rs.8775.000
What about async variants?
Each method has an async variant that runs the function on a separate thread instead of the completing thread.
cf.thenApply(fn); // fn runs on completing thread
cf.thenApplyAsync(fn); // fn runs on ForkJoinPool thread
cf.thenApplyAsync(fn, pool); // fn runs on custom pool
Use non-async for cheap transforms (parseInt, string format). Use async for heavy or blocking work.
How does error handling work in the pipeline?
cf.exceptionally(ex -> fallbackValue); // recover from error
cf.handle((result, ex) -> { // handle both cases
if (ex != null) return "Error";
return "OK: " + result;
});
cf.whenComplete((result, ex) -> log(result, ex)); // observe only
What are common interview follow-up questions?
- What happens if you use thenApply when the function returns a CF? You get CF
>, which is nested and requires double unwrapping. - Can you chain multiple thenCombine calls? Yes, to combine three or more futures.
- What is the difference between thenAccept and thenApply? thenAccept takes a Consumer (no return value), thenApply takes a Function (returns value).
Visual Overview
thenApply (map: sync transform)
CF<String> ──▶ parseInt ──▶ CF<Integer>
"100000" (sync) 100000
thenCompose (flatMap: async chain)
CF<String> ──▶ fetchScoreAsync() ──▶ CF<Integer>
"SAV2001" returns CF<Integer> 750
(flattened!)
thenCombine (merge two parallel futures)
CF<BigDecimal> ──┐
(Rajesh: 150K) ├──▶ BigDecimal::add ──▶ CF<BigDecimal>
CF<BigDecimal> ──┘ (Total: 400K)
(Priya: 250K)
Key Takeaway
CompletableFuture's three core chaining methods serve distinct purposes. thenApply transforms the result synchronously, like map() in streams. Use it for cheap operations: parsing, formatting, arithmetic. thenCompose chains another async operation, like flatMap(). Use it when the function itself returns a CompletableFuture, such as calling another service. thenCombine merges two independent futures running in parallel. Use it when you need results from two unrelated sources.
The decision rule is simple. Does the function return a plain value? Use thenApply. Does it return a CompletableFuture? Use thenCompose. Do you have two independent futures to merge? Use thenCombine.
At National Bank of India, the loan approval pipeline uses all three: thenCompose to fetch the credit score from an external bureau (async call), thenApply to calculate eligibility (quick math), and thenCombine to merge the score with the property valuation (two independent calls in parallel). Total latency: 300ms instead of 600ms sequential.
Remember one sentence: thenApply maps, thenCompose flatMaps, and thenCombine merges two parallel futures.
---