Back to CompletableFuture & Resilience
Q1Question 1 of 19

thenApply vs thenCompose vs thenCombine?

Key Concepts
  • thenApply transforms the result synchronously, like map() in streams: CF<A> to CF<B>.
  • thenCompose chains another async operation, like flatMap(): flattens CF<CF<B>> into CF<B>.
  • thenCombine merges two independent parallel futures: when both complete, combine their results.
  • Use thenApply when the function returns a plain value; use thenCompose when it returns a CompletableFuture.
  • All three have async variants (thenApplyAsync, thenComposeAsync, thenCombineAsync) that run on a pool thread.
  • Error handling with exceptionally(), handle(), and whenComplete() integrates with all three.

Definition

thenApply transforms the result synchronously (like map), thenCompose chains another async operation and flattens (like flatMap), and thenCombine merges two independent parallel futures into one result.
  • thenApply(Function) – transforms the result synchronously; like map() in streams; CF to function returning Integer gives CF.
  • thenCompose(Function>) – chains another async operation; like flatMap() in streams; flattens CF> into 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.

JAVA
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());
    }
}
TEXT
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>.

JAVA
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());
    }
}
TEXT
Credit score: 750

What is thenCombine?

thenCombine merges two independent futures. Both run in parallel, and the BiFunction runs when both complete.

JAVA
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());
    }
}
TEXT
Total: Rs.400000

How do thenApply, thenCompose, and thenCombine compare?

| Method | Input | Function returns | Result | Stream equivalent |
|---|---|---|---|---|
| thenApply | CF | U (plain value) | CF | map() |
| thenCompose | CF | CF (future) | CF | flatMap() |
| thenCombine | CF + CF | V (plain value) | CF | (no equivalent) |

How do you build a full async pipeline?

JAVA
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());
    }
}
TEXT
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.

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

JAVA
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

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

---

Share
Use ← → arrow keys to navigate