use of CompletableFuture.supplyAsync and CompletableFuture.completedFuture within Async annotation in spring boot
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When using @Async in Spring Boot, return CompletableFuture.completedFuture(result) — not CompletableFuture.supplyAsync(). The @Async annotation already runs the method on a separate thread managed by Spring's executor. Using supplyAsync() inside an @Async method spawns a second thread (from ForkJoinPool), which is redundant and wastes resources. completedFuture() wraps the already-computed result in a CompletableFuture without creating another thread, which is the correct pattern.
The Wrong Way: supplyAsync Inside @Async
What happens: Spring runs getUser() on thread-1 (from the async executor). Inside, supplyAsync() submits the lambda to thread-2 (from ForkJoinPool). Thread-1 returns immediately with an incomplete future, and thread-2 does the actual work. This wastes thread-1 entirely.
The Right Way: completedFuture Inside @Async
Spring runs getUser() on its async thread pool. The method does the work, then wraps the result in a CompletableFuture that is immediately complete. The caller gets the future and can chain operations on it.
Setting Up @Async in Spring Boot
Calling @Async Methods
When supplyAsync IS Appropriate
Use supplyAsync when you are NOT using @Async — i.e., in regular synchronous methods where you want to explicitly run code on another thread:
Error Handling
Decision Matrix
| Scenario | Use |
@Async method returning a result | CompletableFuture.completedFuture(result) |
No @Async, need background execution | CompletableFuture.supplyAsync(supplier, executor) |
@Async method returning nothing | void return type (or CompletableFuture<Void>) |
@Async method with error | CompletableFuture.failedFuture(exception) (Java 9+) |
Common Pitfalls
- Using
supplyAsyncinside@Async: This creates two threads for one task. The@Asyncthread sits idle whilesupplyAsyncruns on aForkJoinPoolthread. UsecompletedFutureinstead — the@Asyncmethod is already running asynchronously. - Calling
@Asyncmethods from the same class: Spring's@Asyncworks through proxying. Calling an@Asyncmethod from another method in the same class bypasses the proxy and runs synchronously. Extract the async method to a separate@Servicebean. - Not configuring a thread pool: Without
@EnableAsyncand a custom executor, Spring usesSimpleAsyncTaskExecutorwhich creates a new thread per call with no pooling or queueing. Always configure aThreadPoolTaskExecutorwith bounded pool size and queue capacity. - Forgetting
@EnableAsync: The@Asyncannotation does nothing without@EnableAsyncon a configuration class. The method runs synchronously and the caller receives an already-completed future, which looks correct in tests but defeats the purpose. - Using
supplyAsync()without specifying an executor:CompletableFuture.supplyAsync(supplier)uses the commonForkJoinPool, which is shared across the JVM. Blocking I/O operations (database queries, HTTP calls) on the common pool starve CPU-bound tasks. Always pass a custom executor:supplyAsync(supplier, myExecutor).
Summary
- Use
CompletableFuture.completedFuture(result)inside@Asyncmethods — the async threading is already handled by Spring - Use
CompletableFuture.supplyAsync(supplier, executor)only in non-@Asyncmethods where you need explicit background execution - Always configure
@EnableAsyncwith aThreadPoolTaskExecutorto control pool size and behavior - Never call
@Asyncmethods from within the same class — the proxy is bypassed and the method runs synchronously - Always pass a custom executor to
supplyAsyncto avoid blocking the sharedForkJoinPool

