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.

Shepherd Advanced Notes
Shepherd Advanced Notes
Shepherd Advanced Notes
How to Cut API Latency from 5 s to 0.5 s with CompletableFuture Async Optimizations

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 future

Best‑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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

concurrencyexception handlingCompletableFuturethread poolasynchronous programmingJava 8parallel execution
Shepherd Advanced Notes
Written by

Shepherd Advanced Notes

Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.