2.7 Asynchronous Processing (@Async)
For work that does not need to block the request thread (e.g. email, external API calls, notifications), use @Async to run it on a separate thread and release the request thread quickly.
Reference: Spring Boot 3.2.x
1. @EnableAsync and Basic Usage
@SpringBootApplication
@EnableAsync
public class Application { ... }
@Service
public class NotificationService {
@Async
public void sendEmail(String to, String subject, String body) {
// mail sending logic — runs on a separate thread
mailSender.send(to, subject, body);
}
}
- Methods annotated with @Async run on a different thread.
- Calling
this.sendEmail(...)within the same bean bypasses the proxy and does not run asynchronously. The call must come from another bean.
2. Thread Pool
Configure a thread pool instead of the default SimpleAsyncTaskExecutor (creates a new thread per task) for production use.
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
Use @Async("customExecutor") to target a specific executor bean for individual methods.
3. Return Value — CompletableFuture
Return CompletableFuture<T> to use the async result later.
@Async
public CompletableFuture<OrderResult> createOrderAsync(OrderRequest request) {
OrderResult result = orderService.create(request);
return CompletableFuture.completedFuture(result);
}
The caller can use .get() or .thenAccept(...) etc. to consume the result.
4. Exception Handling
Exceptions from @Async methods do not propagate to the caller. Configure AsyncUncaughtExceptionHandler, or if the return type is Future/CompletableFuture, exceptions are wrapped in ExecutionException when calling .get().
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("Async error @{} {}", method.getName(), params, ex);
}
Only offload heavy tasks to @Async, and tune thread pool size and queue capacity to match your load. Async methods that call DB or external APIs are often combined with timeout and Retry.
sidebar_position: 8
2.8 Scheduling (@Scheduled)
Use @Scheduled for periodic tasks: regular batch jobs, stats aggregation, cache cleanup, etc.
Reference: Spring Boot 3.2.x
1. @EnableScheduling
@SpringBootApplication
@EnableScheduling
public class Application { ... }
2. Fixed Rate / Delay
@Component
public class StatsScheduler {
@Scheduled(fixedRate = 60000) // every 60s (regardless of previous completion)
public void collectStats() {
// collect stats
}
@Scheduled(fixedDelay = 60000) // 60s after previous completion
public void cleanup() {
// cleanup
}
@Scheduled(initialDelay = 10000, fixedRate = 60000) // every 60s, starting 10s after startup
public void sync() {
// sync
}
}
- fixedRate: Interval from start time
- fixedDelay: Interval after previous task completion
- initialDelay: Delay before first run (ms)
3. Cron
Use cron for complex day-of-week / time schedules.
@Scheduled(cron = "0 0 2 * * ?") // every day at 2 AM
public void dailyReport() { ... }
@Scheduled(cron = "0 */10 * * * ?") // every 10 minutes
public void everyTenMinutes() { ... }
Format: second minute hour day month weekday [year]
- Weekday: 0–7 (0 and 7 = Sunday), MON–SUN also accepted
- Spring uses 6 fields (year omitted)
4. Disable by Profile / Property
Use @Profile or @ConditionalOnProperty to run schedules only in certain environments.
@Component
@Profile("!test")
@ConditionalOnProperty(name = "app.scheduler.enabled", havingValue = "true", matchIfMissing = true)
public class StatsScheduler { ... }
5. Single Instance (Distributed)
With multiple servers, use ShedLock or DB/Redis-based lock to prevent duplicate execution. Spring's default @Scheduled does not coordinate across instances.
For long-running tasks, limit the thread pool size with TaskScheduler and combine with @Async to run on a separate thread if needed.
sidebar_position: 12
2.12 Retry and Resilience (Retry / Resilience4j)
External API, DB, and message broker calls can fail due to transient errors. Use Retry and Circuit Breaker to absorb failures and limit blast radius.
Reference: Spring Boot 3.2.x, spring-retry or Resilience4j
1. Spring Retry (@Retryable)
implementation 'org.springframework.retry:spring-retry'
@SpringBootApplication
@EnableRetry
public class Application { ... }
@Service
public class PaymentClient {
@Retryable(
retryFor = { RestClientException.class },
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public PaymentResult callExternalApi(PaymentRequest req) {
return restTemplate.postForObject(...);
}
@Recover // called after all retries are exhausted
public PaymentResult recover(RestClientException e, PaymentRequest req) {
log.error("Payment API call failed, fallback", e);
return PaymentResult.failed("Temporary error");
}
}
- retryFor: Only retry on these exceptions
- maxAttempts: Maximum number of attempts
- backoff: Wait time (delay ms, increasing by multiplier)
- @Recover: Signature must match (Exception, method args...)
2. Resilience4j — Circuit Breaker
Use Resilience4j to block calls for a period on consecutive failures, protecting resources.
implementation 'io.github.resilience4j:resilience4j-spring-boot3'
resilience4j:
circuitbreaker:
instances:
paymentApi:
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
waitDurationInOpenState: 10s
@Service
public class PaymentService {
private final CircuitBreaker circuitBreaker;
private final PaymentClient client;
public PaymentService(CircuitBreakerRegistry registry, PaymentClient client) {
this.circuitBreaker = registry.circuitBreaker("paymentApi");
this.client = client;
}
public PaymentResult pay(PaymentRequest req) {
return circuitBreaker.executeSupplier(() -> client.callExternalApi(req));
}
}
- failureRateThreshold: Circuit opens (OPEN) when failure rate exceeds this value
- waitDurationInOpenState: After this duration in OPEN state, transitions to HALF_OPEN to probe
3. Retry + Circuit Breaker
Resilience4j allows registering both Retry and CircuitBreaker together. A common pattern is to retry a few times with Retry first, then open the CircuitBreaker if the failure rate is still high.
Limit retried exceptions with retryFor and do not retry business errors (4xx etc.). Adjust external dependency timeouts together with RestTemplate/WebClient settings.