CompletableFuture
supplyAsync
completedFuture
@Async
Spring Boot

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

java
1@Service
2public class UserService {
3
4    // WRONG — double threading
5    @Async
6    public CompletableFuture<User> getUser(Long id) {
7        // @Async already runs this on Spring's thread pool
8        // supplyAsync creates ANOTHER thread from ForkJoinPool
9        return CompletableFuture.supplyAsync(() -> {
10            return userRepository.findById(id).orElseThrow();
11        });
12    }
13}

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

java
1@Service
2public class UserService {
3
4    // CORRECT — single thread, result wrapped in CompletableFuture
5    @Async
6    public CompletableFuture<User> getUser(Long id) {
7        User user = userRepository.findById(id).orElseThrow();
8        return CompletableFuture.completedFuture(user);
9    }
10}

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

java
1@Configuration
2@EnableAsync
3public class AsyncConfig implements AsyncConfigurer {
4
5    @Override
6    public Executor getAsyncExecutor() {
7        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
8        executor.setCorePoolSize(5);
9        executor.setMaxPoolSize(10);
10        executor.setQueueCapacity(25);
11        executor.setThreadNamePrefix("async-");
12        executor.initialize();
13        return executor;
14    }
15
16    @Override
17    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
18        return (ex, method, params) ->
19            System.err.println("Async error in " + method.getName() + ": " + ex.getMessage());
20    }
21}
java
1// Application class
2@SpringBootApplication
3@EnableAsync
4public class MyApplication {
5    public static void main(String[] args) {
6        SpringApplication.run(MyApplication.class, args);
7    }
8}

Calling @Async Methods

java
1@RestController
2public class UserController {
3
4    @Autowired
5    private UserService userService;
6
7    @GetMapping("/user/{id}")
8    public CompletableFuture<User> getUser(@PathVariable Long id) {
9        return userService.getUser(id);
10    }
11
12    // Combine multiple async results
13    @GetMapping("/user/{id}/profile")
14    public CompletableFuture<UserProfile> getProfile(@PathVariable Long id) {
15        CompletableFuture<User> userFuture = userService.getUser(id);
16        CompletableFuture<List<Order>> ordersFuture = orderService.getOrders(id);
17        CompletableFuture<Preferences> prefsFuture = prefService.getPreferences(id);
18
19        return CompletableFuture.allOf(userFuture, ordersFuture, prefsFuture)
20            .thenApply(v -> new UserProfile(
21                userFuture.join(),
22                ordersFuture.join(),
23                prefsFuture.join()
24            ));
25    }
26}

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:

java
1@Service
2public class ReportService {
3
4    @Autowired
5    private Executor asyncExecutor;
6
7    // No @Async — manually managing threads
8    public CompletableFuture<Report> generateReport(Long id) {
9        return CompletableFuture.supplyAsync(() -> {
10            // This runs on asyncExecutor's thread
11            return buildReport(id);
12        }, asyncExecutor);  // Use Spring's executor, not ForkJoinPool
13    }
14
15    // Multiple parallel tasks without @Async
16    public CompletableFuture<DashboardData> getDashboard() {
17        CompletableFuture<Stats> statsFuture =
18            CompletableFuture.supplyAsync(this::computeStats, asyncExecutor);
19        CompletableFuture<List<Alert>> alertsFuture =
20            CompletableFuture.supplyAsync(this::fetchAlerts, asyncExecutor);
21
22        return statsFuture.thenCombine(alertsFuture, DashboardData::new);
23    }
24}

Error Handling

java
1@Service
2public class UserService {
3
4    @Async
5    public CompletableFuture<User> getUser(Long id) {
6        try {
7            User user = userRepository.findById(id)
8                .orElseThrow(() -> new UserNotFoundException(id));
9            return CompletableFuture.completedFuture(user);
10        } catch (Exception e) {
11            CompletableFuture<User> future = new CompletableFuture<>();
12            future.completeExceptionally(e);
13            return future;
14        }
15    }
16}
17
18// Caller handles the exception
19@GetMapping("/user/{id}")
20public CompletableFuture<ResponseEntity<User>> getUser(@PathVariable Long id) {
21    return userService.getUser(id)
22        .thenApply(ResponseEntity::ok)
23        .exceptionally(ex -> ResponseEntity.notFound().build());
24}

Decision Matrix

ScenarioUse
@Async method returning a resultCompletableFuture.completedFuture(result)
No @Async, need background executionCompletableFuture.supplyAsync(supplier, executor)
@Async method returning nothingvoid return type (or CompletableFuture<Void>)
@Async method with errorCompletableFuture.failedFuture(exception) (Java 9+)

Common Pitfalls

  • Using supplyAsync inside @Async: This creates two threads for one task. The @Async thread sits idle while supplyAsync runs on a ForkJoinPool thread. Use completedFuture instead — the @Async method is already running asynchronously.
  • Calling @Async methods from the same class: Spring's @Async works through proxying. Calling an @Async method from another method in the same class bypasses the proxy and runs synchronously. Extract the async method to a separate @Service bean.
  • Not configuring a thread pool: Without @EnableAsync and a custom executor, Spring uses SimpleAsyncTaskExecutor which creates a new thread per call with no pooling or queueing. Always configure a ThreadPoolTaskExecutor with bounded pool size and queue capacity.
  • Forgetting @EnableAsync: The @Async annotation does nothing without @EnableAsync on 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 common ForkJoinPool, 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 @Async methods — the async threading is already handled by Spring
  • Use CompletableFuture.supplyAsync(supplier, executor) only in non-@Async methods where you need explicit background execution
  • Always configure @EnableAsync with a ThreadPoolTaskExecutor to control pool size and behavior
  • Never call @Async methods from within the same class — the proxy is bypassed and the method runs synchronously
  • Always pass a custom executor to supplyAsync to avoid blocking the shared ForkJoinPool

Course illustration
Course illustration

All Rights Reserved.