PrepZone Logo
PrepZone

@Async and @Scheduled

Background tasks, cron schedules and thread pool configuration.

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

Thousands of virtual threads multiplex onto a small pool of platform (carrier) threads.

Core annotations and configuration

  • @EnableAsync — activates Spring's async proxy infrastructure; add it to a @Configuration class 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 @Scheduled expressions.
  • @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

Java
@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

Java
@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));
    }
}
Java
@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

Java
@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();
    }
}
Java
# 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 + @EnableScheduling activate the infrastructure; configure pools explicitly in production.
  • @Async methods must be public, on a Spring bean, and called through the proxy — not via this.
  • Name your executor bean and reference it with @Async("bookstoreTaskExecutor") to avoid sharing the default pool.
  • Use @Scheduled(cron = "...") for daily jobs; fixedDelay when 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.