DEV Community

nk sk
nk sk

Posted on

⚡ Advanced `CompletableFuture` Use Cases in Java

In the previous section, we covered the basics of CompletableFuture: chaining, combining, exception handling, and real-world use cases. Now, let’s go one step further and look at advanced features that make it production-ready:


8. Setting Timeouts (orTimeout & completeOnTimeout)

Sometimes tasks hang due to slow APIs or network issues. Instead of blocking forever, CompletableFuture lets you set timeouts.

orTimeout() → fail after timeout

import java.util.concurrent.*;

public class CompletableFutureTimeout {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(3000); } catch (InterruptedException e) {}
            return "Completed!";
        }).orTimeout(2, TimeUnit.SECONDS);

        try {
            System.out.println(future.get());
        } catch (Exception e) {
            System.out.println("Timeout! " + e.getMessage());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

👉 After 2 seconds, the future throws a TimeoutException.

completeOnTimeout() → return default value if timeout

public class CompletableFutureCompleteOnTimeout {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(3000); } catch (InterruptedException e) {}
            return "Data loaded!";
        }).completeOnTimeout("Fallback value", 2, TimeUnit.SECONDS);

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

9. Cancelling a Task

If a task is no longer needed, you can cancel it.

import java.util.concurrent.*;

public class CompletableFutureCancel {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(5000);
                return "Finished!";
            } catch (InterruptedException e) {
                return "Interrupted!";
            }
        });

        Thread.sleep(1000);
        boolean cancelled = future.cancel(true); // tries to stop execution

        System.out.println("Cancelled: " + cancelled);
    }
}
Enter fullscreen mode Exit fullscreen mode

10. Using Custom ExecutorService

By default, CompletableFuture uses the ForkJoinPool.commonPool(). For better control, especially in production, you should provide a custom thread pool.

import java.util.concurrent.*;

public class CompletableFutureCustomExecutor {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(4);

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            return "Running in custom executor: " + Thread.currentThread().getName();
        }, executor);

        System.out.println(future.get());
        executor.shutdown();
    }
}
Enter fullscreen mode Exit fullscreen mode

📌 Why use a custom executor?

  • Prevents blocking the common pool with long tasks.
  • Lets you tune thread counts for CPU-bound vs I/O-bound workloads.

11. Any-of Pattern (anyOf)

When you want the fastest response from multiple tasks.

import java.util.concurrent.*;

public class CompletableFutureAnyOf {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
            sleep(2000);
            return "Result from service 1";
        });

        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return "Result from service 2";
        });

        CompletableFuture<Object> fastest = CompletableFuture.anyOf(f1, f2);

        System.out.println("Winner: " + fastest.get());
    }

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

👉 Output:

Winner: Result from service 2
Enter fullscreen mode Exit fullscreen mode

12. Pipelining with Dependent Async Calls

Imagine fetching user details, then fetching their orders.

public class CompletableFuturePipeline {
    public static void main(String[] args) throws Exception {
        CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return "User123";
        });

        CompletableFuture<String> ordersFuture = userFuture.thenCompose(user -> 
            CompletableFuture.supplyAsync(() -> {
                sleep(1500);
                return "Orders for " + user;
            })
        );

        System.out.println(ordersFuture.get());
    }

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

👉 thenCompose() is used when one async task depends on another.


🔑 Key Takeaways

  • Timeouts prevent tasks from blocking forever.
  • Cancellation helps stop unnecessary tasks.
  • Custom executors give better performance control.
  • anyOf/allOf enable powerful parallel task handling.
  • thenCompose is the go-to for dependent async calls.

✅ Final Thoughts

CompletableFuture is not just about async execution—it’s a framework for orchestrating tasks:

  • Run tasks in parallel
  • Chain dependent pipelines
  • Handle timeouts, retries, errors
  • Customize execution with executors

When used correctly, it helps build high-performance, non-blocking, resilient applications in Java.


Would you like me to also include a full mini-project example (like fetching from 3 services with timeout, fallback, and parallel execution) so you can see how all these features work together in real-world code?

Top comments (0)