Backend Development 8 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Asynchronous Programming in Spring Boot 3 with @Async and CompletableFuture

@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&lt;T&gt; .

<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>
JavaBackend DevelopmentCompletableFutureSpring BootAsync
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.