Async Processing with @Async
Intermediate@Async runs a method in a background thread from a configurable TaskExecutor; combine with CompletableFuture to compose results without blocking the caller.
Overview
@Async is Spring's mechanism for running a method in a background thread without the caller blocking. When you annotate a Spring-managed method with @Async, the proxy intercepts the call and submits it to a TaskExecutor (thread pool) instead of executing it in the caller's thread. The caller gets back immediately — the method runs concurrently. This is ideal for fire-and-forget side effects (sending emails, audit logging, async notifications) and for fan-out patterns where you want to fire multiple independent operations in parallel and join their results. @Async requires @EnableAsync on a @Configuration class and the method must be on a different bean from the caller (same self-invocation limitation as @Transactional).
Basic @Async — Fire-and-Forget
For fire-and-forget tasks (no result needed), annotate the method with @Async and return void. Add @EnableAsync to a configuration class. Spring uses SimpleAsyncTaskExecutor by default — this creates a new thread per call with no pooling, which is fine for development but not for production. Always configure a ThreadPoolTaskExecutor for production.
// 1. Enable async processing
@SpringBootApplication
@EnableAsync
public class App { }
// 2. Async method — runs in background thread, caller does not wait
@Service
public class NotificationService {
@Async // executes in background thread
public void sendOrderConfirmation(String email, String orderId) {
// Caller returns immediately; this runs concurrently
emailClient.send(email, "Order confirmed: " + orderId);
log.info("Sent confirmation to {} on thread {}", email,
Thread.currentThread().getName());
}
}
// 3. Caller — does not block
@Service
@RequiredArgsConstructor
public class OrderService {
private final NotificationService notificationService;
@Transactional
public Order placeOrder(OrderRequest req) {
Order order = orderRepo.save(new Order(req));
// Returns immediately — email is sent asynchronously
notificationService.sendOrderConfirmation(req.getEmail(), order.getId().toString());
return order;
}
}CompletableFuture — Async with Return Value
When you need the result, return CompletableFuture<T>. The caller can then compose multiple async calls in parallel with CompletableFuture.allOf() and join results — a powerful fan-out pattern for aggregating data from multiple services.
@Service
public class ProductEnrichmentService {
@Async
public CompletableFuture<Stock> fetchStock(Long productId) {
Stock stock = inventoryClient.getStock(productId); // slow external call
return CompletableFuture.completedFuture(stock);
}
@Async
public CompletableFuture<PriceInfo> fetchPrice(Long productId) {
PriceInfo price = pricingClient.getPrice(productId);
return CompletableFuture.completedFuture(price);
}
}
@Service
@RequiredArgsConstructor
public class ProductDetailService {
private final ProductEnrichmentService enrichmentService;
// Fan-out: fire both calls in parallel, join results
public ProductDetail getDetail(Long productId) throws Exception {
CompletableFuture<Stock> stockFuture = enrichmentService.fetchStock(productId);
CompletableFuture<PriceInfo> priceFuture = enrichmentService.fetchPrice(productId);
// Wait for both to complete
CompletableFuture.allOf(stockFuture, priceFuture).join();
// Sequential: 500ms + 300ms = 800ms
// Parallel: max(500ms, 300ms) = 500ms ← 37% faster
return ProductDetail.of(stockFuture.get(), priceFuture.get());
}
}ThreadPoolTaskExecutor — Production Configuration
The default SimpleAsyncTaskExecutor creates a new thread per call — no pooling, no backpressure. For production, configure a ThreadPoolTaskExecutor with appropriate core/max pool sizes and a bounded queue. Spring Boot auto-configuration provides a default ThreadPoolTaskExecutor bean (spring.task.execution.pool.*) when @EnableAsync is active.
# application.yml — Spring Boot auto-configured thread pool
spring:
task:
execution:
pool:
core-size: 5 # threads always kept alive
max-size: 20 # max concurrent threads
queue-capacity: 100 # tasks queued before rejecting
keep-alive: 60s # idle threads above core-size survive for 60s
thread-name-prefix: async-
# Or configure programmatically for full control:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(5);
exec.setMaxPoolSize(20);
exec.setQueueCapacity(100);
exec.setThreadNamePrefix("async-");
exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // fallback
exec.initialize();
return exec;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("Unhandled exception in @Async method {}", method.getName(), ex);
}
}Key Points to Remember
- 1@EnableAsync on a @Configuration class activates @Async support; without it, @Async annotations are silently ignored.
- 2@Async works via AOP proxy — self-invocation (calling an @Async method on the same bean) bypasses the proxy and runs synchronously.
- 3Return CompletableFuture<T> for async methods that produce results; combine with CompletableFuture.allOf() for parallel fan-out.
- 4Never use the default SimpleAsyncTaskExecutor in production — configure a ThreadPoolTaskExecutor with bounded pool and queue.
- 5Exceptions thrown in void @Async methods are silently swallowed; configure AsyncUncaughtExceptionHandler to log or alert.
- 6@Transactional and @Async on the same method is a trap — the transaction commits before the method returns, and the async work runs outside it.
Interview Questions
Sign in to ask AriaHow does @Async work in Spring Boot and what does @EnableAsync do?
Why does @Async not work when calling the method from the same bean?
How would you execute two independent external API calls in parallel and combine their results?
What happens to exceptions thrown in a void @Async method?
What are the risks of combining @Async and @Transactional on the same method?
Ask Aria about Async Processing with @Async
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.