Master Asynchronous Programming in Spring Boot 3 with @Async and CompletableFuture
This article demonstrates how to enable and use Spring Boot 3's @Async annotation together with Java's CompletableFuture to create custom thread pools, run multiple asynchronous REST calls concurrently, aggregate their results, and handle exceptions, providing a complete backend solution for high‑performance APIs.
@Async and CompletableFuture are powerful tools for asynchronous processing in Spring Boot 3.2.5.
@Async is a Spring annotation that marks a method to be executed asynchronously in a thread pool, improving performance and responsiveness.
CompletableFuture, introduced in Java 8, represents a result of a computation that may not have completed yet and offers a rich API for callbacks, composition, and error handling.
Combining @Async with CompletableFuture enables efficient asynchronous task handling.
1. Enable Async Support
Apply @EnableAsync on a configuration class and define a custom Executor bean.
<code>@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
int core = Runtime.getRuntime().availableProcessors();
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(core);
executor.setMaxPoolSize(core);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("PackAsync-");
executor.initialize();
return executor;
}
}
</code>You may also rely on Spring's default executor.
2. Create Async Tasks
Annotate public methods with @Async("asyncExecutor") and return CompletableFuture<T> .
<code>@Async("asyncExecutor")
public CompletableFuture<EmployeeNames> task() {
// TODO
}
</code>Multiple async tasks can run concurrently and be combined with CompletableFuture.allOf(...).join() .
<code>CompletableFuture.allOf(asyncMethodOne, asyncMethodTwo, asyncMethodThree).join();
</code>3. Call Async Tasks from a RestController
Define three REST endpoints that provide data.
<code>@RestController
public class EmployeeController {
@GetMapping("/addresses")
public EmployeeAddresses addresses() { /* TODO */ }
@GetMapping("/phones")
public EmployeePhone phones() { /* TODO */ }
@GetMapping("/names")
public EmployeeNames names() { /* TODO */ }
}
</code>In a service, use RestTemplate to fetch each endpoint asynchronously.
<code>@Service
public class AsyncService {
private final RestTemplate restTemplate;
public AsyncService(RestTemplate restTemplate) { this.restTemplate = restTemplate; }
@Async("asyncExecutor")
public CompletableFuture<EmployeeNames> names() {
EmployeeNames data = restTemplate.getForObject("http://localhost:8080/names", EmployeeNames.class);
return CompletableFuture.completedFuture(data);
}
// similar methods for addresses() and phones()
}
</code>Expose a combined API that waits for all futures and returns a DTO.
<code>@RestController
public class AsyncController {
private final AsyncService asyncService;
public AsyncController(AsyncService service) { this.asyncService = service; }
@GetMapping("/profile/infos")
public EmployeeDTO infos() throws Exception {
CompletableFuture<EmployeeAddresses> addresses = asyncService.addresses();
CompletableFuture<EmployeeNames> names = asyncService.names();
CompletableFuture<EmployeePhone> phones = asyncService.phones();
CompletableFuture.allOf(addresses, names, phones).join();
return new EmployeeDTO(addresses.get(), names.get(), phones.get());
}
}
</code>The total response time equals the longest individual call, greatly improving the endpoint's performance.
4. Exception Handling
When async methods return Future , handle exceptions with try‑catch after get() . For void async methods, implement AsyncUncaughtExceptionHandler to capture uncaught exceptions.
<code>@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(AsyncExceptionHandler.class);
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
logger.error("Unexpected asynchronous exception at: " + method.getDeclaringClass().getName() + "." + method.getName(), ex);
}
}
}
</code>Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.