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.

@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;
    }
}

You may also rely on Spring's default executor.

2. Create Async Tasks

Annotate public methods with @Async("asyncExecutor") and return CompletableFuture<T>.

@Async("asyncExecutor")
public CompletableFuture<EmployeeNames> task() {
    // TODO
}

Multiple async tasks can run concurrently and be combined with CompletableFuture.allOf(...).join().

CompletableFuture.allOf(asyncMethodOne, asyncMethodTwo, asyncMethodThree).join();

3. Call Async Tasks from a RestController

Define three REST endpoints that provide data.

@RestController
public class EmployeeController {
    @GetMapping("/addresses")
    public EmployeeAddresses addresses() { /* TODO */ }

    @GetMapping("/phones")
    public EmployeePhone phones() { /* TODO */ }

    @GetMapping("/names")
    public EmployeeNames names() { /* TODO */ }
}

In a service, use RestTemplate to fetch each endpoint asynchronously.

@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()
}

Expose a combined API that waits for all futures and returns a DTO.

@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());
    }
}

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.

@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);
        }
    }
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backend-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

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.