Async Processing in Spring Boot
Some operations shouldn’t block the request — sending emails, processing files, calling external APIs. Spring’s @Async runs them in a separate thread.
Setup
@SpringBootApplication
@EnableAsync
public class BookstoreApiApplication { }
@Async — Fire and Forget
@Service
public class NotificationService {
@Async
public void sendWelcomeEmail(String email) {
// Runs in a separate thread — doesn't block the caller
log.info("Sending email to {} on thread {}", email, Thread.currentThread().getName());
// Simulate slow email sending
Thread.sleep(3000);
log.info("Email sent to {}", email);
}
}
@Service
public class AuthService {
private final NotificationService notificationService;
public UserResponse register(RegisterRequest request) {
AppUser user = createUser(request);
notificationService.sendWelcomeEmail(user.getEmail()); // returns immediately
return UserResponse.from(user); // doesn't wait for email
}
}
@Async with CompletableFuture
@Service
public class BookEnrichmentService {
@Async
public CompletableFuture<BookDetails> fetchFromGoogleBooks(String isbn) {
// Call external API
BookDetails details = restTemplate.getForObject(
"https://www.googleapis.com/books/v1/volumes?q=isbn:" + isbn,
BookDetails.class);
return CompletableFuture.completedFuture(details);
}
@Async
public CompletableFuture<List<Review>> fetchReviews(Long bookId) {
List<Review> reviews = reviewClient.getReviews(bookId);
return CompletableFuture.completedFuture(reviews);
}
}
@Service
public class BookService {
public BookFullResponse getFullBookInfo(Long id) throws Exception {
Book book = bookRepository.findById(id).orElseThrow();
// Run in parallel
CompletableFuture<BookDetails> detailsFuture =
enrichmentService.fetchFromGoogleBooks(book.getIsbn());
CompletableFuture<List<Review>> reviewsFuture =
enrichmentService.fetchReviews(id);
// Wait for both
CompletableFuture.allOf(detailsFuture, reviewsFuture).join();
return new BookFullResponse(
book, detailsFuture.get(), reviewsFuture.get()
);
}
}
Custom Thread Pool
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "emailExecutor")
public Executor emailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("email-");
executor.initialize();
return executor;
}
@Bean(name = "apiExecutor")
public Executor apiExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("api-");
executor.initialize();
return executor;
}
}
@Async("emailExecutor")
public void sendEmail(String email) { }
@Async("apiExecutor")
public CompletableFuture<BookDetails> fetchDetails(String isbn) { }
Async Exception Handling
@Component
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("Async error in {}: {}", method.getName(), ex.getMessage(), ex);
}
}
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
}
Rules of @Async
| Rule | Why |
|---|---|
| Must be on a public method | Proxy-based AOP needs public access |
| Must be called from a different class | Self-invocation bypasses the proxy |
Don’t call @Async from the same bean |
Won’t be async |
Return void or CompletableFuture |
Other return types are ignored |
Key Takeaways
@EnableAsync+@Async= method runs in a separate thread.- Return
CompletableFutureto get async results. CompletableFuture.allOf()runs multiple async tasks in parallel.- Create custom thread pools for different workloads.
@Asynconly works across beans — not for self-invocation.
What’s Next?
Next: Scheduling — @Scheduled, cron expressions, and distributed locking.