Chaturmind
LearnDSASystem DesignBlogPremium
Sign inGet started
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML

Company

  • Blog
  • Premium
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Java Concurrency & Multithreading

Threads & Runnable

  • Introduction to Threads
  • ExecutorService & Thread Pools

Synchronization & Memory Model

  • synchronized and Locks
  • volatile and the Memory Model
  • CompletableFuture

Concurrent Collections

  • ConcurrentHashMap & CopyOnWriteArrayList
Chaturmind
← Java Concurrency & Multithreading

Threads & Runnable

  • Introduction to Threads
  • ExecutorService & Thread Pools

Synchronization & Memory Model

  • synchronized and Locks
  • volatile and the Memory Model
  • CompletableFuture

Concurrent Collections

  • ConcurrentHashMap & CopyOnWriteArrayList
HomeLearnJavaJava ConcurrencyAsync Programming
✓ FreeAdvanced· 14 min read

CompletableFuture

Write non-blocking async pipelines with CompletableFuture, thenApply, thenCompose, and allOf.

Published February 14, 2025


CompletableFuture

CompletableFuture<T> (Java 8+) is a composable async computation. Unlike Future, it supports non-blocking callbacks, chaining, and combining multiple async operations.

Creating CompletableFutures

// Run async, no return value
CompletableFuture<Void> cf1 = CompletableFuture.runAsync(() -> {
    System.out.println("async task");
});

// Async with return value
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
    return fetchUserFromDB("u1"); // runs in ForkJoinPool.commonPool()
});

// With custom executor
ExecutorService pool = Executors.newFixedThreadPool(4);
CompletableFuture<String> cf3 = CompletableFuture.supplyAsync(
    () -> fetchUser("u1"), pool
);

Chaining with thenApply, thenAccept, thenRun

CompletableFuture.supplyAsync(() -> "user_123")
    .thenApply(userId -> fetchUser(userId))      // transform: String → User
    .thenApply(user -> user.getEmail())           // transform: User → String
    .thenAccept(email -> sendEmail(email))        // consume: String → void
    .thenRun(() -> log.info("Email sent"))        // run after: void → void
    .exceptionally(e -> {
        log.error("Failed", e);
        return null;
    });

thenCompose — flatten nested futures

// thenApply wraps the result: CompletableFuture<CompletableFuture<User>>
// thenCompose flattens it:    CompletableFuture<User>

CompletableFuture<User> future = CompletableFuture
    .supplyAsync(() -> "user_123")
    .thenCompose(userId -> fetchUserAsync(userId)); // returns CF<User>

Combining Multiple Futures

// Wait for all to complete
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> fetchName());
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> fetchAge());

CompletableFuture<String> combined = f1.thenCombine(f2,
    (name, age) -> name + " is " + age);

// Wait for a list of futures
List<CompletableFuture<String>> futures = userIds.stream()
    .map(id -> CompletableFuture.supplyAsync(() -> fetchUser(id)))
    .toList();

CompletableFuture<Void> allDone = CompletableFuture.allOf(
    futures.toArray(new CompletableFuture[0])
);

// Collect results after all complete
allDone.thenApply(v -> futures.stream()
    .map(CompletableFuture::join)
    .toList());

// Complete when the FIRST one finishes
CompletableFuture<Object> anyDone = CompletableFuture.anyOf(
    futures.toArray(new CompletableFuture[0])
);

Error Handling

CompletableFuture.supplyAsync(() -> riskyOperation())
    .exceptionally(ex -> {
        log.error("Operation failed", ex);
        return defaultValue(); // recovery value
    })
    .handle((result, ex) -> {
        // Called whether succeeded or failed
        if (ex != null) return handleError(ex);
        return transform(result);
    });

Real-World Pattern: Parallel API Calls

public UserProfileDto getProfile(String userId) {
    CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> userService.findById(userId));
    CompletableFuture<List<Order>> ordersFuture =
        CompletableFuture.supplyAsync(() -> orderService.findByUser(userId));
    CompletableFuture<UserStats> statsFuture =
        CompletableFuture.supplyAsync(() -> statsService.getStats(userId));

    // Wait for all three in parallel
    return CompletableFuture.allOf(userFuture, ordersFuture, statsFuture)
        .thenApply(v -> new UserProfileDto(
            userFuture.join(),
            ordersFuture.join(),
            statsFuture.join()
        ))
        .join(); // block for the final result
}

Interview Tips

  1. Know the difference between thenApply (synchronous transform in callback thread) and thenApplyAsync (transform in a new thread).
  2. join() is like get() but throws unchecked exceptions — preferred in streams.
  3. CompletableFuture.allOf() returns CompletableFuture<Void> — you must call join() on each individual future to get results.

Previous

volatile and the Memory Model

Next

ConcurrentHashMap & CopyOnWriteArrayList

AI Tutor

Lesson: CompletableFuture

Quick actions

AI responses can be inaccurate. Verify critical information.