← All articles
Backend1 min read

Concurrency with CompletableFuture

Callbacks nest, threads block, and orchestration gets messy fast. CompletableFuture lets you compose async work into readable pipelines.

S

Swapnika Voora

Author

Blocking a thread to wait for a result wastes a resource that could be doing other work. CompletableFuture models a value that will arrive later and lets you chain transformations without ever calling get() prematurely.

Compose instead of block

Each stage runs when the previous one completes. Nothing blocks the calling thread until you explicitly join at the very end.

OrderService.java
CompletableFuture<Order> future = CompletableFuture
    .supplyAsync(() -> loadUser(userId), executor)
    .thenApply(this::validate)
    .thenCompose(user -> CompletableFuture.supplyAsync(() -> createOrder(user)))
    .exceptionally(ex -> {
        log.error("order failed", ex);
        return Order.empty();
    });

Combine parallel work

When two calls are independent, fire both and merge the results with thenCombine. The total latency becomes the slower of the two, not the sum.

Combine.java
var price = CompletableFuture.supplyAsync(() -> pricing.quote(sku));
var stock = CompletableFuture.supplyAsync(() -> inventory.check(sku));
price.thenCombine(stock, (p, s) -> new Availability(p, s)).join();

Takeaways

  • Prefer thenCompose for chaining futures and thenApply for plain values.
  • Always supply your own Executor instead of the common pool for I/O work.
  • Handle failure with exceptionally or handle so errors do not vanish.

Never mix blocking get() calls inside a chain — it defeats the entire purpose and can deadlock a bounded pool.

#java#concurrency#async

More in Backend