In modern application development, responsiveness and scalability are key. One way to achieve this is by leveraging asynchronous programming. In Java, CompletableFuture combined with Spring’s @Async provides a powerful way to write non-blocking, concurrent code. This post explores using CompletableFuture with async methods in a Spring Boot application.

🚀 Why Use CompletableFuture?
The CompletableFuture class, introduced in Java 8, represents a future result of an asynchronous computation. Unlike Future, it allows non-blocking operations and supports chaining multiple tasks.
With Spring Boot, we can annotate methods with @Async to run them in a separate thread. When used with CompletableFuture, we get a fluent, readable, and efficient way to perform async operations.
⚙️ Maven Dependencies
Make sure your project uses Spring Boot with Spring Context. Here’s your pom.xml dependency:
org.springframework.boot
spring-boot-starter
🛠️ Enable Async Support
In your main Spring Boot class or configuration class:
package com.kscodes.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class AsyncApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncApplication.class, args);
}
}
📦 Sample Service with CompletableFuture and @Async
Let’s create a service to simulate a long-running task using CompletableFuture with async methods.
package com.kscodes.springboot.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class DataService {
@Async
public CompletableFuture getDataFromSource1() throws InterruptedException {
Thread.sleep(2000); // Simulate delay
return CompletableFuture.completedFuture("Data from source 1");
}
@Async
public CompletableFuture getDataFromSource2() throws InterruptedException {
Thread.sleep(3000); // Simulate delay
return CompletableFuture.completedFuture("Data from source 2");
}
}
🎯 Controller to Trigger Async Operations
Here’s a simple controller that invokes both async methods and combines the results using thenCombine.
package com.kscodes.springboot.controller;
import com.kscodes.springboot.service.DataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
@RestController
public class AsyncController {
@Autowired
private DataService dataService;
@GetMapping("/combine-data")
public CompletableFuture fetchCombinedData() {
CompletableFuture source1 = dataService.getDataFromSource1();
CompletableFuture source2 = dataService.getDataFromSource2();
return source1.thenCombine(source2, (data1, data2) -> data1 + " + " + data2);
}
}
🧪 Output
When you hit the /combine-data endpoint, both async methods will run in parallel and the combined result will be returned once both complete.
Example Output:
Data from source 1 + Data from source 2
Execution time: ~3 seconds instead of 5 seconds, thanks to async parallelism.
⚠️ Notes and Best Practices
- @EnableAsync must be present to enable async behavior.
- @Async methods should return
CompletableFuturefor proper async chaining. - Exception handling is tricky—wrap logic in
.handle()or use.exceptionally(). - Async methods must not be private, and should be called via a Spring-managed bean (i.e., no
this.method()).
📌 Use Case: API Aggregation
Using CompletableFuture with async methods is ideal for:
- Fetching data from multiple APIs simultaneously.
- Processing large datasets in parallel.
- Background jobs or deferred processing tasks.
🧹 Cleaning Up with Custom Executor
By default, Spring uses SimpleAsyncTaskExecutor. You can define your own thread pool:
package com.kscodes.springboot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("AsyncThread-");
executor.initialize();
return executor;
}
}
Then in your service:
@Async("taskExecutor")
public CompletableFuture getDataFromSource1() { ... }
🧾 Conclusion
Combining CompletableFuture with async methods is a powerful technique to make your Spring Boot applications scalable and performant. By understanding and leveraging this pattern, you can drastically reduce latency in I/O-bound applications.
✅ You’ve now learned how to effectively use CompletableFuture with async methods in Spring Boot.
✅ Summary
- ✅ Use
@Asyncon service methods. - ✅ Return
CompletableFuturefor non-blocking processing. - ✅ Combine futures using
.thenCombine,.thenApply,.exceptionally, etc. - ✅ Customize thread pools for better resource management.