DEV Community

nk sk
nk sk

Posted on

🚀 Mastering `CompletableFuture` in Java: Use Cases and Example Codes

Working with asynchronous programming in Java used to be tricky with just Thread, Runnable, or ExecutorService. Since Java 8, CompletableFuture has become one of the most powerful APIs for handling asynchronous tasks with clean, fluent, and non-blocking code.

In this blog, we’ll explore:

  • Basics of CompletableFuture
  • Real-world use cases
  • Chaining and combining futures
  • Exception handling
  • Running multiple tasks in parallel

1. Getting Started with CompletableFuture

A CompletableFuture represents a future result of an asynchronous computation. It can be manually completed or executed asynchronously.

Example: Running an async task

import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("Task executed in: " + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        future.get(); // wait until completion
    }
}
Enter fullscreen mode Exit fullscreen mode

📌 Key point: runAsync() runs a Runnable (no return value), while supplyAsync() runs a Supplier (with return value).


2. Returning Results with supplyAsync

import java.util.concurrent.*;

public class CompletableFutureReturn {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) { }
            return "Hello from " + Thread.currentThread().getName();
        });

        System.out.println(future.get()); // blocks until result is available
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Chaining with thenApply, thenAccept, thenRun

  • thenApply() → transform result and return new value
  • thenAccept() → consume result (no return)
  • thenRun() → run after completion (ignores result)
import java.util.concurrent.*;

public class CompletableFutureChaining {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> "Data")
                .thenApply(data -> data + " processed")
                .thenAccept(System.out::println)
                .thenRun(() -> System.out.println("Pipeline finished!"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

Data processed
Pipeline finished!
Enter fullscreen mode Exit fullscreen mode

4. Combining Multiple Futures

Example: thenCombine (merge two results)

import java.util.concurrent.*;

public class CompletableFutureCombine {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");

        CompletableFuture<String> combined = future1.thenCombine(future2, (a, b) -> a + " " + b);

        System.out.println(combined.get()); // Hello World
    }
}
Enter fullscreen mode Exit fullscreen mode

Example: allOf (wait for multiple tasks)

import java.util.concurrent.*;

public class CompletableFutureAllOf {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Task1");
        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Task2");
        CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> "Task3");

        CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
        all.join();

        System.out.println(f1.get() + ", " + f2.get() + ", " + f3.get());
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Handling Exceptions

Using exceptionally

public class CompletableFutureException {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            if (true) throw new RuntimeException("Something went wrong!");
            return "OK";
        }).exceptionally(ex -> "Recovered from: " + ex.getMessage());

        System.out.println(future.get()); // Recovered from: Something went wrong!
    }
}
Enter fullscreen mode Exit fullscreen mode

Using handle

public class CompletableFutureHandle {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            if (true) throw new RuntimeException("Error occurred!");
            return "Success";
        }).handle((result, ex) -> {
            if (ex != null) return "Fallback value";
            return result;
        });

        System.out.println(future.get()); // Fallback value
    }
}
Enter fullscreen mode Exit fullscreen mode

6. Real-World Use Case: Fetching Data from Multiple APIs

Imagine you want to fetch product details and pricing from different services in parallel.

import java.util.concurrent.*;

public class ProductService {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> productFuture = CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return "Product details";
        });

        CompletableFuture<String> priceFuture = CompletableFuture.supplyAsync(() -> {
            sleep(1500);
            return "Price details";
        });

        CompletableFuture<String> finalResult =
                productFuture.thenCombine(priceFuture, (product, price) -> product + " + " + price);

        System.out.println("Final Result: " + finalResult.get());
    }

    private static void sleep(int ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { }
    }
}
Enter fullscreen mode Exit fullscreen mode

7. When to Use CompletableFuture

✅ Best for:

  • Running I/O-bound tasks in parallel (API calls, DB queries)
  • Building pipelines of transformations
  • Handling dependent async tasks
  • Aggregating results from multiple computations

❌ Avoid for:

  • Purely CPU-bound heavy computations (better use ForkJoinPool or parallel streams)
  • Extremely short tasks (overhead may outweigh benefits)

📝 Conclusion

CompletableFuture simplifies asynchronous programming in Java by making code:

  • Readable (fluent chaining)
  • Composable (combine tasks easily)
  • Robust (built-in exception handling)

By using patterns like thenCombine, allOf, and exceptionally, you can build efficient non-blocking pipelines for real-world applications.


👉 Do you want me to also include advanced use cases like timeouts (orTimeout), cancellation, and using a custom ExecutorService in this blog?

Top comments (0)