Why this matters
- Sending order confirmation emails or generating PDF receipts from a REST endpoint blocks the HTTP response until the slow work finishes — async moves that work off the request thread.
- Scheduled jobs power recurring BookStore tasks: nightly inventory syncs, abandoned-cart reminders, and stale reservation cleanup.
- Misconfigured thread pools are a common production incident: too few threads create backlogs, too many threads exhaust memory.
Platform threads (carriers)
VT-1Blocked on I/O
VT-2Running
VT-3Blocked on I/O
Blocked virtual threads release their carrier — other virtual threads continue
Core annotations and configuration
@EnableAsync— activates Spring's async proxy infrastructure; add it to a@Configurationclass or your main application class.@Async— marks a method to run on the task executor instead of the caller's thread; must be on a public method of a Spring-managed bean.@EnableScheduling— turns on the scheduler that evaluates@Scheduledexpressions.@Scheduled— runs a method on a fixed rate, fixed delay, or cron expression.TaskExecutor/ThreadPoolTaskExecutor— the bean behind@Async; you define pool size, queue capacity, and thread naming.
Enable async and scheduling
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig {
@Bean(name = "bookstoreTaskExecutor")
public Executor bookstoreTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("bookstore-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
Fire-and-forget email after checkout
@Service
public class OrderNotificationService {
private final MailSender mailSender;
public OrderNotificationService(MailSender mailSender) {
this.mailSender = mailSender;
}
@Async("bookstoreTaskExecutor")
public void sendOrderConfirmation(Order order) {
mailSender.send(order.getCustomerEmail(),
"Order " + order.getId() + " confirmed",
buildReceiptBody(order));
}
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final OrderNotificationService notificationService;
public OrderResponse placeOrder(PlaceOrderRequest request) {
Order saved = orderRepository.save(buildOrder(request));
notificationService.sendOrderConfirmation(saved); // returns immediately
return OrderResponse.from(saved);
}
}
Cron jobs for housekeeping
@Component
public class InventoryMaintenanceJobs {
private final ReservationRepository reservationRepository;
public InventoryMaintenanceJobs(ReservationRepository reservationRepository) {
this.reservationRepository = reservationRepository;
}
@Scheduled(cron = "0 0 2 * * *") // 02:00 every day
public void releaseExpiredReservations() {
int released = reservationRepository.deleteExpiredBefore(Instant.now());
log.info("Released {} expired book reservations", released);
}
@Scheduled(fixedRate = 300_000) // every 5 minutes
public void refreshBestsellerCache() {
bestsellerService.recomputeRankings();
}
}
# application.yml
spring:
task:
scheduling:
pool:
size: 2
execution:
pool:
core-size: 4
max-size: 16
queue-capacity: 200
Quick recall
Everything you need if you only revisit this box.
@EnableAsync+@EnableSchedulingactivate the infrastructure; configure pools explicitly in production.@Asyncmethods must be public, on a Spring bean, and called through the proxy — not viathis.- Name your executor bean and reference it with
@Async("bookstoreTaskExecutor")to avoid sharing the default pool. - Use
@Scheduled(cron = "...")for daily jobs;fixedDelaywhen the next run should wait until the previous one finishes. - Async work loses request-scoped context — pass IDs explicitly; MDC and security context do not propagate automatically.
Test yourself
Answer these before moving on — recall is what makes it stick.