Spring Boot: @Async & Asynchronous Processing
Spring's @Async annotation makes it easy to execute methods on a separate thread pool without writing thread management code yourself. The calling thread returns immediately; the work continues in the background. This is useful for fire-and-forget operations (sending emails, notifications) or for running multiple tasks concurrently.
Contents
@EnableAsync
Add @EnableAsync@Configuration@SpringBootApplication
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfig {
// Define a custom Executor bean here (optional — defaults to SimpleAsyncTaskExecutor)
}
SimpleAsyncTaskExecutorThreadPoolTaskExecutor@Async on void Methods
For fire-and-forget operations where you do not need to wait for the result.
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class NotificationService {
private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
// Runs on the async thread pool — caller returns immediately
@Async
public void sendWelcomeEmail(String email, String username) {
log.info("Sending welcome email to {} on thread {}", email, Thread.currentThread().getName());
emailClient.send(email, "Welcome " + username, buildEmailBody(username));
log.info("Welcome email sent to {}", email);
}
}
// Calling code — returns immediately, email is sent in background
@Service
public class RegistrationService {
private final NotificationService notificationService;
public User register(RegisterRequest request) {
User user = userRepository.save(new User(request));
notificationService.sendWelcomeEmail(request.getEmail(), request.getUsername());
return user; // returned before email sending completes
}
}
@Async with CompletableFuture Return
Return a CompletableFuture
import java.util.concurrent.CompletableFuture;
@Service
public class ReportService {
@Async
public CompletableFuture<Report> generateReport(Long userId) {
log.info("Generating report on {}", Thread.currentThread().getName());
Report report = reportRepository.buildReport(userId);
return CompletableFuture.completedFuture(report);
}
}
// Caller — run two async tasks concurrently and combine results
@Service
public class DashboardService {
private final ReportService reportService;
private final StatsService statsService;
public DashboardData getDashboard(Long userId) throws Exception {
// Both start concurrently on different threads
CompletableFuture<Report> reportFuture = reportService.generateReport(userId);
CompletableFuture<Stats> statsFuture = statsService.getStats(userId);
// Wait for both to complete
CompletableFuture.allOf(reportFuture, statsFuture).join();
return new DashboardData(reportFuture.get(), statsFuture.get());
}
}
Custom ThreadPoolTaskExecutor
Define your own executor to control the pool size, queue capacity and thread names for @Async
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // minimum threads always alive
executor.setMaxPoolSize(20); // max threads under load
executor.setQueueCapacity(100); // queue size before new threads are created
executor.setThreadNamePrefix("async-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
// Multiple executors — use name in @Async("name")
@Bean(name = "emailExecutor")
public Executor emailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setThreadNamePrefix("email-");
executor.initialize();
return executor;
}
}
// Use a specific named executor
@Async("emailExecutor")
public void sendEmail(String to, String subject, String body) {
emailClient.send(to, subject, body);
}
// Default executor (taskExecutor bean if defined, otherwise SimpleAsyncTaskExecutor)
@Async
public void processInBackground(String data) {
processingService.process(data);
}
Exception Handling in Async Methods
Exceptions thrown by voidAsyncUncaughtExceptionHandler
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.scheduling.annotation.AsyncConfigurer;
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("Async exception in {}.{}(): {}",
method.getDeclaringClass().getSimpleName(),
method.getName(),
ex.getMessage(), ex);
// could also push to alerting service
};
}
}
For CompletableFuture
@Async
public CompletableFuture<Report> generateReport(Long userId) {
try {
Report report = reportRepository.buildReport(userId);
return CompletableFuture.completedFuture(report);
} catch (Exception e) {
return CompletableFuture.failedFuture(e); // caller can handle via .exceptionally()
}
}
Pitfalls
1. Self-invocation bypasses the proxy
@Service
public class MyService {
// WRONG — calling async() from within the same class bypasses the AOP proxy
public void doWork() {
this.async(); // @Async has NO effect here
}
@Async
public void async() { /* runs synchronously */ }
}
// FIX — inject the bean into itself or call from another bean
2. @Async on private methods: Spring AOP only intercepts public methods. Always make async methods public.
3. Security context not propagated: the Spring Security SecurityContextDelegatingSecurityContextAsyncTaskExecutor
4. Transaction context not propagated: @Transactional@Async