Spring Threading & Async Patterns¶
Asynchronous processing in Spring: @Async methods, CompletableFuture, TaskExecutor configuration, and virtual thread integration for scalable web applications.
@Async Annotation¶
Spring's @Async annotation executes methods asynchronously on a thread pool without blocking the caller.
Basic Setup¶
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // Min threads
executor.setMaxPoolSize(20); // Max threads
executor.setQueueCapacity(100); // Queue size
executor.setThreadNamePrefix("async-"); // Thread name
executor.initialize();
return executor;
}
}
// Alternatively, use virtual threads (Java 21+)
@Configuration
@EnableAsync
public class VirtualThreadAsyncConfig {
@Bean(name = "taskExecutor")
public TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor("vt-"); // Virtual threads
}
}
@Async Method Examples¶
@Service
public class EmailService {
// 1. Void return (fire-and-forget)
@Async
public void sendEmailAsync(String to, String subject, String body) {
// Runs on thread pool, caller doesn't wait
Thread.sleep(2000); // Simulate I/O
System.out.println("Email sent to " + to);
}
// 2. Return Future<T> (caller can wait)
@Async
public Future<Boolean> sendEmailWithResult(String to, String subject, String body) {
try {
Thread.sleep(2000);
return new AsyncResult<>(true); // Success
} catch (Exception e) {
return new AsyncResult<>(false); // Failure
}
}
// 3. Return CompletableFuture<T> (recommended)
@Async
public CompletableFuture<String> sendEmailWithFuture(String to, String subject, String body) {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
return "Email sent to " + to;
} catch (InterruptedException e) {
return "Failed: " + e.getMessage();
}
});
}
}
@RestController
@RequestMapping("/api")
public class OrderController {
@Autowired
private EmailService emailService;
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody Order order) {
// Save order
Order saved = orderRepository.save(order);
// Send email asynchronously (non-blocking)
emailService.sendEmailAsync(
order.getCustomerEmail(),
"Order Confirmed",
"Your order #" + saved.getId() + " is confirmed"
);
// Return immediately (email still sending in background)
return ResponseEntity.status(201).body(saved);
}
}
Key Points:
- ✅ @Async void methods: fire-and-forget
- ✅ @Async Future<T>: caller can wait/check result
- ✅ @Async CompletableFuture<T>: recommended, composable
- ❌ Returns from @Async must be void or Future (not plain T)
- ❌ Calling @Async method from same class bypasses async (self-invocation issue)
CompletableFuture in Spring¶
Powerful API for composing asynchronous operations without nested callbacks.
Basic Operations¶
@Service
public class PaymentService {
private final ExternalPaymentGateway gateway;
private final NotificationService notificationService;
// 1. Simple async operation
public CompletableFuture<PaymentResponse> processPayment(Order order) {
return CompletableFuture.supplyAsync(() -> {
return gateway.charge(order); // Blocking I/O on executor
});
}
// 2. Chain operations (non-blocking)
public CompletableFuture<String> processPaymentWithNotification(Order order) {
return processPayment(order)
.thenApply(response -> {
// Transform result
return "Payment " + response.getStatus();
})
.thenApply(status -> {
// More transformations
return status.toUpperCase();
})
.exceptionally(ex -> {
// Handle errors
return "Payment failed: " + ex.getMessage();
});
}
// 3. Combine multiple async operations
public CompletableFuture<OrderSummary> getOrderWithPaymentAndNotification(Order order) {
CompletableFuture<PaymentResponse> paymentFuture = processPayment(order);
CompletableFuture<Notification> notificationFuture =
notificationService.createNotificationAsync(order);
return paymentFuture.thenCombine(notificationFuture, (payment, notification) -> {
// Both completed, combine results
return new OrderSummary(order, payment, notification);
});
}
// 4. Wait for first to complete
public CompletableFuture<PaymentResponse> processWithFallback(Order order) {
CompletableFuture<PaymentResponse> primary = processPayment(order);
CompletableFuture<PaymentResponse> fallback = processFallbackPayment(order);
return primary.acceptEither(fallback, response -> {
System.out.println("Payment succeeded: " + response.getStatus());
}).thenApply(v -> primary.getNow(null)); // Return the winner
}
// 5. Chain async -> sync -> async
public CompletableFuture<OrderConfirmation> completeOrder(Order order) {
return processPayment(order)
.thenApply(payment -> {
// Sync: save payment result
Order updated = order.withPaymentId(payment.getId());
return orderRepository.save(updated);
})
.thenCompose(saved -> {
// Async: send async notification
return notificationService.notifyCustomerAsync(saved);
});
}
}
Common CompletableFuture methods:
- supplyAsync(): Create future with value
- thenApply(): Transform result
- thenAccept(): Consume result (void)
- thenCombine(): Combine two futures
- thenCompose(): Chain async → async
- acceptEither(): React to first completion
- allOf(): Wait for all futures
- anyOf(): Wait for any future
- exceptionally(): Handle errors
- handle(): Handle result or exception
REST Controller with Async¶
Non-blocking REST endpoints¶
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
// 1. Simple async endpoint
@PostMapping
public CompletableFuture<ResponseEntity<Order>> createOrder(@RequestBody Order order) {
return orderService.createOrderAsync(order)
.thenApply(saved -> ResponseEntity.status(201).body(saved))
.exceptionally(ex -> ResponseEntity.status(500).build());
}
// 2. Async with query/processing
@GetMapping("/{orderId}/status")
public CompletableFuture<ResponseEntity<OrderStatus>> getOrderStatus(@PathVariable Long orderId) {
return orderService.getOrderAsync(orderId)
.thenApply(order -> new OrderStatus(order))
.thenApply(ResponseEntity::ok)
.exceptionally(ex -> ResponseEntity.notFound().build());
}
// 3. Parallel async requests
@GetMapping("/{orderId}/details")
public CompletableFuture<ResponseEntity<OrderDetails>> getOrderDetails(@PathVariable Long orderId) {
CompletableFuture<Order> orderFuture = orderService.getOrderAsync(orderId);
CompletableFuture<List<Item>> itemsFuture = orderService.getOrderItemsAsync(orderId);
CompletableFuture<Payment> paymentFuture = orderService.getPaymentAsync(orderId);
return CompletableFuture.allOf(orderFuture, itemsFuture, paymentFuture)
.thenApply(v -> new OrderDetails(
orderFuture.join(),
itemsFuture.join(),
paymentFuture.join()
))
.thenApply(ResponseEntity::ok);
}
}
Benefits:
- ✅ Non-blocking responses
- ✅ Efficient thread usage
- ✅ Composable async chains
- ✅ Timeout support (via orTimeout())
TaskExecutor Configuration¶
Standard Spring TaskExecutor Types¶
@Configuration
@EnableAsync
public class AsyncConfiguration {
// 1. ThreadPoolTaskExecutor (most common)
@Bean(name = "defaultExecutor")
public TaskExecutor defaultExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("spring-async-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
// 2. SimpleAsyncTaskExecutor (unbounded threads, use with caution)
@Bean(name = "simpleExecutor")
public TaskExecutor simpleExecutor() {
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
executor.setThreadNamePrefix("simple-async-");
return executor;
}
// 3. Virtual Thread Executor (Java 21+)
@Bean(name = "virtualExecutor")
public TaskExecutor virtualExecutor() {
return new SimpleAsyncTaskExecutor("vt-"); // Auto-detects virtual threads
}
// 4. SyncTaskExecutor (for testing, blocks caller)
@Bean(name = "syncExecutor")
public TaskExecutor syncExecutor() {
return new SyncTaskExecutor(); // Synchronous, no parallelism
}
// 5. Multiple executors for different purposes
@Bean(name = "emailExecutor")
public TaskExecutor emailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.initialize();
return executor;
}
@Bean(name = "reportExecutor")
public TaskExecutor reportExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(1000);
executor.initialize();
return executor;
}
}
// Use specific executor
@Service
public class MultiExecutorService {
@Async("emailExecutor")
public CompletableFuture<String> sendEmail(String to) {
// Runs on email-specific executor
return CompletableFuture.completedFuture("Email sent");
}
@Async("reportExecutor")
public CompletableFuture<String> generateReport() {
// Runs on report-specific executor
return CompletableFuture.completedFuture("Report generated");
}
}
Virtual Threads in Spring (Java 21+)¶
Virtual threads dramatically simplify async handling:
// Before (Java 8-20): Complex async chains needed
@Service
public class LegacyOrderService {
@Async
public CompletableFuture<OrderResponse> processOrder(Order order) {
return getPaymentAsync(order)
.thenCompose(payment -> saveOrderAsync(order, payment))
.thenCompose(saved -> notifyAsync(saved));
}
}
// After (Java 21+): Can write blocking code, Spring handles with virtual threads
@Service
public class ModernOrderService {
@Autowired
private OrderRepository orderRepository;
@Async // Runs on virtual thread, looks synchronous!
public void processOrder(Order order) {
// Blocking code that looks synchronous but runs on virtual thread
Payment payment = paymentGateway.charge(order); // Blocks virtual thread
Order saved = orderRepository.save(order.withPayment(payment));
notificationService.notify(saved); // Another blocking call
// All happen on virtual threads, no context switching overhead!
}
}
// Spring Boot config
@Configuration
public class VirtualThreadConfig {
@Bean
public TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor("vt-"); // Virtual threads
}
@Bean
public TomcatProtocolHandlerCustomizer<?> tomcatProtocolHandlerCustomizer() {
// Use virtual threads for HTTP requests (Spring 6.1+)
return protocolHandler -> protocolHandler.setExecutor(new SimpleAsyncTaskExecutor("vt-"));
}
}
Virtual threads advantages in Spring: - ✅ Write imperative code (no callback chains) - ✅ Millions of concurrent requests - ✅ Simple debugging (clear stack traces) - ✅ Eliminates reactive frameworks complexity
Common Patterns & Best Practices¶
Pattern 1: Timeout Handling¶
public CompletableFuture<PaymentResponse> processWithTimeout(Order order) {
return processPayment(order)
.orTimeout(5, TimeUnit.SECONDS) // Fail if takes > 5s
.exceptionally(ex -> {
if (ex instanceof TimeoutException) {
return new PaymentResponse(order.getId(), "TIMEOUT");
}
return new PaymentResponse(order.getId(), "ERROR");
});
}
Pattern 2: Retry with Exponential Backoff¶
public CompletableFuture<PaymentResponse> processWithRetry(Order order) {
return CompletableFuture.supplyAsync(() -> {
int attempts = 0;
while (attempts < 3) {
try {
return paymentGateway.charge(order);
} catch (TemporaryFailureException e) {
attempts++;
if (attempts < 3) {
Thread.sleep((long) Math.pow(2, attempts) * 1000); // Exponential backoff
}
}
}
throw new PaymentException("Failed after 3 attempts");
});
}
Pattern 3: Cascading Failures¶
public CompletableFuture<OrderConfirmation> processOrderResilient(Order order) {
return processPayment(order)
.thenCompose(payment -> {
if (!payment.isSuccessful()) {
return CompletableFuture.failedFuture(
new PaymentException("Payment failed")
);
}
return saveOrder(order);
})
.exceptionallyCompose(ex -> {
// Fallback: mark as pending
return markOrderPending(order)
.thenApply(v -> new OrderConfirmation("PENDING"));
});
}
@Async vs. CompletableFuture vs. Virtual Threads¶
| Feature | @Async | CompletableFuture | Virtual Threads |
|---|---|---|---|
| Syntax | Simple decorator | Composable chains | Looks synchronous |
| Return | void or Future |
Always explicit | Imperative code |
| Composition | Limited | Excellent | N/A (no futures) |
| Error Handling | Try-catch per method | exceptionally() |
Normal try-catch |
| Testing | Need mock executor | Mock with TestExecutor | Native support |
| Java 21+ | Use with virtual threads | Still valid | Recommended |
Interview Talking Points¶
- @Async vs. CompletableFuture? @Async is simpler for basic async; CompletableFuture for complex chains
- Virtual threads impact on Spring? Eliminates need for reactive frameworks for I/O-bound apps
- TaskExecutor sizing? I/O-bound = large pool or virtual threads; CPU-bound = CPU count
- Self-invocation issue? Can't call @Async from same class (proxy limitation); use constructor injection workaround
- Exception handling in async? Use
exceptionally()for CompletableFuture; @Async void needs custom handling - When to use @Async void? Fire-and-forget logging, notifications
- Spring WebFlux vs. Virtual Threads? WebFlux for complex reactive chains; virtual threads for simpler I/O
See also: Threading Deep-Dive & Virtual Threads, Spring Boot Advanced