CompletableFuture vs Async
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
CompletableFuture and Spring @Async solve different problems and operate at different layers. CompletableFuture is a JDK API for composing asynchronous computations: chaining, combining, transforming, and recovering from async results. @Async is a Spring mechanism that dispatches a method invocation onto a thread pool through a proxy. They are complementary, not competing alternatives, and production code frequently uses both together.
What CompletableFuture Does
CompletableFuture is part of java.util.concurrent (since Java 8). It represents a future result of an asynchronous computation and provides a fluent API for expressing what should happen when that result becomes available.
The key capabilities are:
| Method | Purpose |
supplyAsync() | Run a supplier on the common ForkJoinPool (or a custom executor) |
thenApply() | Transform the result when it completes |
thenCompose() | Chain another async operation (flatMap) |
thenCombine() | Combine results from two independent futures |
exceptionally() | Provide a fallback on failure |
allOf() / anyOf() | Wait for all or any of multiple futures |
None of this requires Spring. It works in any Java application.
What Spring @Async Does
@Async is a Spring annotation that tells the framework to execute the annotated method on a task executor instead of the caller's thread. It works through AOP proxies: Spring intercepts the method call, wraps it in a Runnable or Callable, and submits it to an executor.
@Async requires @EnableAsync on a configuration class:
Without @EnableAsync, the annotation is silently ignored and methods execute synchronously.
The Core Difference
The cleanest mental model:
| Concern | Tool |
| Where does the code run? (execution boundary) | @Async |
| What happens with the result? (composition, chaining, error handling) | CompletableFuture |
@Async answers "run this somewhere else." CompletableFuture answers "and then do this with the result." They address orthogonal concerns, which is why using them together is the standard pattern, not an antipattern.
Using Them Together
A typical service aggregation pattern:
The @Async annotations on the service methods handle the execution boundary (which thread pool runs the work). The CompletableFuture API handles the orchestration (wait for all three, then combine).
Executor Configuration
Both tools default to the common ForkJoinPool or Spring's SimpleAsyncTaskExecutor, neither of which is appropriate for production I/O-bound work. Always configure named executors:
Reference the executor by name in @Async("ioExecutor") or pass it explicitly to CompletableFuture.supplyAsync(supplier, ioExecutor).
The Self-Invocation Trap
@Async works through Spring AOP proxies. When a method within the same bean calls another @Async method directly, the call bypasses the proxy and executes synchronously:
Workarounds:
- Inject self: Inject the bean into itself so the call goes through the proxy.
- Extract to a separate bean: Move the
@Asyncmethod to a different service. - Use
CompletableFuturedirectly: CallCompletableFuture.supplyAsync()with an explicit executor, which does not depend on proxies.
CompletableFuture does not have this limitation because it is standard Java code, not proxy-dependent.
Exception Handling Comparison
Error handling differs significantly:
For @Async methods with void return type, unhandled exceptions are silently swallowed unless you register a custom handler:
This is another reason to prefer CompletableFuture return types on @Async methods: the caller can handle errors through the future itself.
When to Use Which
| Scenario | Recommendation |
| Pure computation chaining, no Spring | CompletableFuture only |
| Spring service that should run on a specific executor | @Async returning CompletableFuture |
| Fan-out to multiple services, combine results | CompletableFuture.allOf() / thenCombine() |
| Fire-and-forget background task in Spring | @Async with void return (with exception handler) |
| Need retry, timeout, or circuit breaker logic | CompletableFuture with explicit handling or a resilience library |
Common Pitfalls
- Treating
@Asyncas a replacement forCompletableFuture. It is not.@Asyncdoes not provide chaining, combination, or structured error recovery by itself. - Calling
.join()or.get()immediately after creating a future. This blocks the calling thread and turns async code back into synchronous code, defeating the purpose. - Forgetting
@EnableAsyncon the configuration class. Without it,@Asyncannotations are silently ignored and everything runs synchronously. - Self-invocation of
@Asyncmethods within the same bean. The call bypasses the Spring proxy and executes on the caller's thread. - Using the default executor for I/O-bound work. The default
ForkJoinPoolorSimpleAsyncTaskExecutor(which creates a new thread per task) will degrade under load. Configure named thread pools sized for your workload. - Returning
voidfrom@Asyncmethods without configuring anAsyncUncaughtExceptionHandler. Exceptions are silently lost.
Summary
CompletableFutureis a JDK API for composing, chaining, and error-handling async results.@Asyncis a Spring mechanism for running a method on a configured executor via AOP proxy.- They are complementary:
@Asynchandles "where it runs",CompletableFuturehandles "what to do with the result." - Always configure named executors. Default thread pools are unsuitable for production.
- Watch for the self-invocation trap with
@Asyncand always enable@EnableAsync. - Prefer
CompletableFuturereturn types overvoidon@Asyncmethods for proper error propagation.
Related reading
- CompletableFuture vs Spring Transactions
- CompletableFutureT class join vs get
- Completion handlers and return values
- Concept of promises in Java
- Computational Complexity of TreeSet methods in Java
- Concatenating null strings in Java
- Concurrency Atomic and volatile in C11 memory model
- Concurrency of Kafka streams topology with multiple output topics

OOD Fundamentals
Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.