How to Cut API Latency from 5 s to 0.5 s with CompletableFuture Async Optimizations
The article explains how to dramatically reduce the response time of an API that must call multiple downstream services by converting serial calls into parallel ones using Java 8's CompletableFuture, covering its relationship to Future, core APIs, task composition, exception handling, and practical best‑practice recommendations.
Problem: Multiple Service Calls Cause High Latency
In many projects a single request may need to invoke N other services—e.g., fetching user info, product details, logistics, and recommendations—before aggregating the result. Executing these calls serially makes the overall response very slow.
Parallel Execution with CompletableFuture
When the calls have no strict ordering, they can be executed in parallel. For example, fetching product details and logistics can run simultaneously, greatly improving response speed.
Obtain user information first, then call product‑detail and logistics services.
After both product‑detail and logistics succeed, call the recommendation service.
Future vs CompletableFuture
The Future interface represents an asynchronous computation but lacks composition capabilities; its get() method blocks. Java 8 introduced CompletableFuture, which implements Future and CompletionStage, adding non‑blocking composition, functional programming support, and richer APIs.
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}Key CompletableFuture APIs
Creating CompletableFuture Instances
Instances can be created with the new operator or via static factory methods such as runAsync() and supplyAsync(). The latter accept an optional custom Executor for better thread‑pool control.
CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();
// or using a factory method
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "hello!");Combining Tasks
thenCompose()links two futures sequentially, passing the first result to the second. thenCombine() runs two futures in parallel and merges their results. acceptEither() triggers when either of two futures completes.
// Sequential composition
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "hello!")
.thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "world!"));
// Parallel combination
CompletableFuture<String> combined = CompletableFuture.supplyAsync(() -> "hello!")
.thenCombine(CompletableFuture.supplyAsync(() -> "world!"), (s1, s2) -> s1 + s2);
// Either‑of composition
task1.acceptEitherAsync(task2, res -> System.out.println("First result: " + res));Processing Results
After a computation finishes, thenApply(), thenAccept(), and thenRun() allow further processing. The former receives the result, the latter runs a side‑effect without accessing the result.
CompletableFuture.completedFuture("hello!")
.thenApply(s -> s + "world!")
.thenAccept(System.out::println); // prints "hello!world!"Exception Handling
Use handle(), exceptionally(), or whenComplete() to process failures without losing the exception context.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (true) throw new RuntimeException("Computation error!");
return "hello!";
}).handle((res, ex) -> res != null ? res : "fallback");
future.exceptionally(ex -> {
System.out.println(ex.toString());
return "fallback";
});Running Multiple Futures
CompletableFuture.allOf()waits for all supplied futures to finish, while anyOf() returns as soon as any one completes.
CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2);
all.join(); // blocks until both are done
CompletableFuture<Object> any = CompletableFuture.anyOf(future1, future2);
System.out.println(any.get()); // prints the result of the first completed futureBest‑Practice Recommendations
Use a custom ThreadPoolExecutor instead of the default ForkJoinPool.commonPool() to avoid thread‑starvation.
Avoid blocking get(); if necessary, supply a timeout to prevent indefinite waits.
Handle exceptions with whenComplete, exceptionally, or handle to keep error information visible.
Choose the appropriate composition method: thenCompose for dependent tasks, thenCombine for independent parallel tasks, and acceptEither when only the first result matters.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
