CompletableFuture vs Async
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
CompletableFuture and Spring @Async are often compared as if they solve the same problem, but they operate at different layers. CompletableFuture is a Java API for composing asynchronous computations, while @Async is a Spring feature for dispatching a method invocation onto an executor.
What CompletableFuture Actually Does
CompletableFuture gives you a fluent way to chain, combine, transform, and recover from asynchronous work. It is part of the JDK, not Spring, and it is useful even in plain Java applications.
The important part is composition. You can express "run this, then transform the result, then combine it with another future, then handle errors" without blocking the current thread.
What Spring @Async Actually Does
@Async tells Spring to execute a method on a task executor through a proxy. It is about where and how the method runs, not about how later computations are composed.
If the call goes through the Spring proxy, the method runs asynchronously on the configured executor.
The Practical Difference
The cleanest mental model is:
- '
@Asyncchooses an execution boundary' - '
CompletableFuturemodels the async result and its downstream workflow'
That means they are complementary, not mutually exclusive. A Spring service method can be annotated with @Async and still return a CompletableFuture that the caller later combines with other futures.
Using Them Together
This is a common pattern in service aggregation:
Here the service methods may use @Async, but the orchestration is done with CompletableFuture.
Executor Choice Matters
Using either tool badly often comes down to executor configuration. The default executor may be fine for demos, but production code usually needs named pools sized for the workload.
If your async work blocks on databases or remote services, executor tuning affects latency more than the API choice itself.
A Spring-Specific Gotcha
@Async works through Spring proxies, so self-invocation does not trigger asynchronous execution. If one method in a bean calls another @Async method on the same bean directly, the second call is just a normal method call.
That surprises many developers:
- External bean call through proxy: async
- Same-class direct call: not async
CompletableFuture does not have that proxy limitation because it is just Java code you create explicitly.
Common Pitfalls
The biggest mistake is treating @Async as a replacement for CompletableFuture. It is not. It does not provide chaining, combination, or structured error handling by itself.
Another issue is calling .join() immediately after creating a future. That turns asynchronous code back into a blocking call and often defeats the whole point.
Developers also forget about executor saturation. A perfectly correct CompletableFuture pipeline can still perform badly if all tasks are competing for an undersized shared pool.
Finally, be careful with @Async self-invocation. If the method call does not pass through a Spring proxy, no async dispatch happens.
Summary
- '
CompletableFutureis a JDK API for composing asynchronous workflows.' - '
@Asyncis a Spring mechanism for executing a method on an executor.' - They are complementary and are often best used together.
- Configure executors deliberately for the type of work being performed.
- Watch for Spring proxy behavior, especially self-invocation of
@Asyncmethods.

