Scheduled Tasks and Asynchronous Processing
Scheduled tasks and asynchronous processing are the keys to boosting backend efficiency—schedule what needs to be scheduled and run in parallel what can be run in parallel, so users don't have to wait unnecessarily.
1. What You'll Learn
@EnableSchedulingand Cron / FixedRate / FixedDelay Expressions- Asynchronous execution of
@EnableAsyncand@Async - Custom Thread Pool
ThreadPoolTaskExecutorConfiguration - Asynchronous Exception Handling
AsyncUncaughtExceptionHandler - Alice implemented a scheduled task to automatically cancel orders that have not been paid within 30 minutes
2. A True Story from a Business Developer
(1) Pain Point: Backlog of Time-Out Orders
Alice discovered a large backlog of unpaid orders in OrderFlow—users were placing orders but not paying, which locked up inventory and prevented other users from making purchases. Charlie reported that there were approximately 2,000 time-out orders per day, collectively locking up inventory worth 50,000 USD. Previously, orders were canceled manually by checking the database every 30 minutes, which was inefficient and prone to oversights.
(2) Solutions for @Scheduled
Spring Boot includes built-in scheduled tasks that can be implemented with a single annotation:
@Scheduled(fixedRate = 5 * 60 * 1000) // Every 5 minutes
public void cancelExpiredOrders() {
List<Order> expired = orderRepository.findByStatusAndCreatedAtBefore(
"PENDING", Instant.now().minus(30, ChronoUnit.MINUTES));
expired.forEach(order -> cancelOrder(order));
log.info("Cancelled {} expired orders", expired.size());
}
(3) Revenue
After Alice implemented the scheduled task, expired orders are automatically cleared every 5 minutes, inventory is automatically released, and the backlog of expired orders has dropped from 2,000 per day to 0. At the same time, by using @Async to switch notification emails to asynchronous delivery, the response time for the order placement API dropped from 800 ms to 50 ms.
3. @Scheduled Scheduled Tasks
(1) Three Scheduling Methods
| Method | Parameter Notes | Meaning | Example |
|---|---|---|---|
| FixedRate | fixedRate |
Fixed interval (counting from the last time) | Run every 5 minutes |
| FixedDelay | fixedDelay |
Fixed delay (from the end of the last session) | Wait 5 minutes after the end of the last session |
| Cron | cron |
Cron expression | Runs at 2:00 a.m. every day |
gantt
title FixedRate vs FixedDelay Timing
dateFormat X
axisFormat %s
section FixedRate
Task A (5min) :a1, 0, 5
Task B (5min) :a2, 5, 8
Task C (5min) :a3, 10, 14
section FixedDelay
Task A (5min) :b1, 0, 5
wait 3min :b2, 5, 8
Task B (5min) :b3, 8, 13
wait 3min :b4, 13, 16
| Dimension | FixedRate | FixedDelay | Cron |
|---|---|---|---|
| Execution Interval | Fixed (regardless of previous duration) | Fixed (after the previous one finishes) | Exact Time |
| Tasks Overlap | May Overlap | Do Not Overlap | May Overlap |
| Use Cases | Polling | Data Processing | Scheduled Batch Processing |
(1) ▶ Example: FixedRate Scheduled Task
@Component
@EnableScheduling
public class OrderScheduleTask {
private final OrderService orderService;
private static final Logger log = LoggerFactory.getLogger(OrderScheduleTask.class);
public OrderScheduleTask(OrderService orderService) {
this.orderService = orderService;
}
@Scheduled(fixedRate = 5 * 60 * 1000) // Every 5 minutes
public void cancelExpiredOrders() {
log.info("Starting expired order cleanup...");
int cancelled = orderService.cancelExpiredOrders(Duration.ofMinutes(30));
log.info("Cancelled {} expired orders", cancelled);
}
}
Output:
// Execution Successful
(2) ▶ Example: Cron Expressions for Scheduled Tasks
@Component
public class DailyReportTask {
@Scheduled(cron = "0 0 2 * * ?") // Every day at 2:00 AM
public void generateDailyReport() {
// Generate daily order statistics report
}
@Scheduled(cron = "0 0 0 1 * ?") // First day of every month at midnight
public void generateMonthlyReport() {
// Generate monthly report
}
}
Output:
// Execution Successful
| Cron Field | Meaning | Valid Values |
|---|---|---|
| Seconds | 0-59 | |
| Min | Minutes | 0-59 |
| Time | Hours | 0-23 |
| Day | Day of the month | 1-31 |
| Month | 1-12 | |
| Day of the Week | Day of the week | 0-7 (0/7 = Sun) |
| Common Cron Expressions | Meaning |
|---|---|
0 0 2 * * ? |
Every day at 2 a.m. |
0 */5 * * * ? |
Every 5 minutes |
0 0 0 1 * ? |
Midnight on the 1st of every month |
0 0 9-17 * * MON-FRI |
Weekdays, 9:00 a.m.-5:00 p.m. (on the hour) |
4. @Async Asynchronous Processing
(1) Synchronous vs. Asynchronous
(1) ▶ Example: Sending Notifications in Parallel (Blocking)
@Service
public class OrderService {
public Order createOrder(CreateOrderRequest request) {
Order order = saveOrder(request);
emailService.sendConfirmation(order); // Blocks for 500ms
smsService.sendNotification(order); // Blocks for 200ms
return order; // Total: 700ms wasted
}
}
Output:
// Execution Successful
(2) ▶ Example: Sending Notifications Asynchronously (Non-Blocking)
@Service
@EnableAsync
public class NotificationService {
@Async
public void sendOrderConfirmation(Order order) {
// This runs in a separate thread
// Does not block the caller
emailService.sendConfirmation(order);
log.info("Confirmation email sent for order {}", order.getId());
}
@Async
public CompletableFuture<String> sendSmsNotification(Order order) {
smsService.sendNotification(order);
return CompletableFuture.completedFuture("SMS sent");
}
}
// In OrderService
@Service
public class OrderService {
private final NotificationService notificationService;
public Order createOrder(CreateOrderRequest request) {
Order order = saveOrder(request);
notificationService.sendOrderConfirmation(order); // Non-blocking
return order; // Returns immediately
}
}
Output:
// Execution Successful
| Dimension | Synchronous | Asynchronous |
|---|---|---|
| Call Method | Block and wait for result | Return immediately; execute in the background |
| Response Time | Total Time for All Operations | Time for Core Business Operations Only |
| Error Handling | Direct try-catch Handling | AsyncUncaughtExceptionHandler |
| Use Cases | Operations That Require Waiting for Results | Notifications, Logs, Non-Critical Operations |
5. Custom Thread Pool
(1) Issues with the Default Thread Pool
Spring Boot uses SimpleAsyncTaskExecutor by default, which creates a new thread for each task and places no limit on the number of threads, which may result in an OutOfMemoryError.
(1) ▶ Example: Custom ThreadPoolTaskExecutor
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("orderflow-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("Async exception in method {}: {}", method.getName(), ex.getMessage(), ex);
};
}
}
Output:
// Execution Successful
| Parameter | Meaning | Recommended Value |
|---|---|---|
corePoolSize |
Number of Core Threads (Resident) | Number of CPU Cores |
maxPoolSize |
Maximum number of threads | Number of CPU cores x 2 |
queueCapacity |
Queue Capacity | 100-1,000 |
threadNamePrefix |
Thread Name Prefix | Project Name-async- |
rejectedExecutionHandler |
Rejection Policy | CallerRunsPolicy |
| Rejection Strategy | Behavior | Applicable Scenarios |
|---|---|---|
AbortPolicy |
Throws RejectedExecutionException | Default, fast failure |
CallerRunsPolicy |
Executed by the caller's thread | Downgraded but not discarded |
DiscardPolicy |
Silent Discard | Tolerable Loss |
DiscardOldestPolicy |
Discard the oldest task | Prioritize real-time performance |
6. Asynchronous Exception Handling
(1) ▶ Example: AsyncUncaughtExceptionHandler
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
log.error("Async method [{}] threw exception: {}",
method.getName(), throwable.getMessage(), throwable);
// Could also publish to monitoring system
};
}
}
Output:
// Execution Successful
@Async void methods are not propagated to the caller and must be handled using AsyncUncaughtExceptionHandler. Exceptions thrown by methods that return CompletableFuture can be handled using future.exceptionally().
7. Comprehensive Example: OrderFlow Timeout Cancellation + Asynchronous Notification
// OrderScheduleTask.java
package com.orderflow.schedule;
import com.orderflow.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class OrderScheduleTask {
private static final Logger log = LoggerFactory.getLogger(OrderScheduleTask.class);
private final OrderService orderService;
public OrderScheduleTask(OrderService orderService) {
this.orderService = orderService;
}
@Scheduled(fixedRateString = "${orderflow.order.expiry-check-interval:300000}")
public void cancelExpiredOrders() {
int timeoutMinutes = 30;
int cancelled = orderService.cancelExpiredOrders(Duration.ofMinutes(timeoutMinutes));
if (cancelled > 0) {
log.info("Cancelled {} orders expired for {} minutes", cancelled, timeoutMinutes);
}
}
@Scheduled(cron = "0 0 2 * * ?")
public void generateDailyReport() {
log.info("Generating daily order report...");
orderService.generateDailyReport();
}
}
// NotificationService.java
package com.orderflow.service;
import com.orderflow.model.Order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class NotificationService {
private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
@Async
public void sendOrderConfirmation(Order order) {
// Simulate email sending
log.info("Sending confirmation email for order {}", order.getId());
}
@Async
public void sendCancellationNotice(Order order) {
log.info("Sending cancellation notice for order {}", order.getId());
}
@Async
public CompletableFuture<String> sendSmsNotification(Order order) {
log.info("Sending SMS for order {}", order.getId());
return CompletableFuture.completedFuture("SMS sent for order " + order.getId());
}
}
// AsyncConfig.java
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfig implements AsyncConfigurer {
private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(5);
exec.setMaxPoolSize(20);
exec.setQueueCapacity(100);
exec.setThreadNamePrefix("orderflow-async-");
exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
exec.setWaitForTasksToCompleteOnShutdown(true);
exec.setAwaitTerminationSeconds(60);
exec.initialize();
return exec;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("Async error in {}: {}", method.getName(), ex.getMessage(), ex);
}
}
❓ FAQ
TaskScheduler thread pool, or delegate time-consuming operations to the thread pool using @Async.fixedRate and fixedDelay for long-running tasks?fixedRate counts down from the start time of the previous task; if a task takes longer than the interval, it will run consecutively or even overlap with other tasks. fixedDelay counts from the end time of the previous task, ensuring a fixed interval between tasks so they do not overlap.ScheduledTaskRegistrar to programmatically register tasks, or use the Configuration Center to dynamically refresh them. A simple solution: fixedRateString = "${interval}"—control it via external configuration.CompletableFuture and one that returns void?void method is "fire-and-forget"; the caller is not concerned with the result, and exceptions are handled by AsyncUncaughtExceptionHandler. CompletableFuture methods allow for chained handling of results and exceptions, giving the caller more control.📖 Summary
@ScheduledThree scheduling methods: fixedRate (fixed interval), fixedDelay (fixed delay), cron (exact time)@AsyncPrevents time-consuming operations from blocking the caller; suitable for non-critical operations such as notifications and logging- The default thread pool places no limit on the number of threads; in a production environment, you must configure it yourself
ThreadPoolTaskExecutor AsyncUncaughtExceptionHandlerHandling Exceptions in void Asynchronous Methods- When deploying multiple instances of a scheduled task, a distributed lock (such as ShedLock) is required to prevent duplicate executions.
@AsyncInterchange of similar methods does not work; you need to decompose the class or inject a proxy.
📝 Exercises
-
Basic Problem (Difficulty ⭐): Implement a scheduled task for OrderFlow to automatically cancel time-out orders. Every 5 minutes, check for and cancel any PENDING orders that have not been paid for 30 minutes.
-
Advanced Exercise (Difficulty: ⭐⭐): Modify the order confirmation email and SMS notifications to be sent asynchronously using @Async, customize the thread pool (core=5, max=20), and verify that the response time of the order placement API has been significantly reduced.
-
Challenge (Difficulty: ⭐⭐⭐): Integrate ShedLock to implement a distributed lock for scheduled tasks, ensuring that when OrderFlow is deployed across multiple instances, only one instance executes a scheduled task at any given time. Consider the lock granularity and timeout design.



