10 - Spring Events and Async Notes
A beginner-to-advanced guide to Spring Events,
@EventListener, asynchronous processing with@Async, task scheduling, and executor configuration for Spring Professional Certification candidates. Covers Spring Framework 6 and Spring Boot 3 concepts.
Table of Contents
- Overview
- Spring Events
- Why Use Events?
- Creating an Event
- Publishing an Event
- Listening to Events With
@EventListener - Multiple Listeners
- Event Listener Ordering
- Conditional Event Listeners
- Synchronous Event Handling
- Asynchronous Event Handling
- Transactional Event Listeners
- Event Traps
- Async Processing
- Async Return Types
- Important
@AsyncRules - Executors
- Executor Configuration Meaning
- Rejection Policies
- Scheduling
- Fixed Rate vs Fixed Delay
- Cron Scheduling
- Scheduling Rules and Traps
- Custom Scheduler Executor
- Practical Example: Order Flow
- When to Use Events, Async, or Scheduling
- Best Practices
- Interview Questions and Answers
- Quick Revision Notes
- One-Page Cheat Sheet
1. Overview
Spring provides built-in support for:
- Events: decouple publishers and listeners inside an application.
- Async processing: run methods in separate threads.
- Scheduling: run tasks periodically or at a fixed time.
- Executors: control thread pools used by async and scheduled tasks.
Controller / Service
|
v
Publishes Event
|
v
Spring Event Multicaster
|
+--> Listener 1
+--> Listener 2
+--> Listener 3
2. Spring Events
Spring events are a way to notify other parts of the application that something happened.
Examples:
- User registered
- Order placed
- Payment completed
- Report generated
- File uploaded
Events help reduce direct coupling between components.
3. Why Use Events?
Without events:
@Service
public class OrderService {
private final EmailService emailService;
private final InventoryService inventoryService;
private final AuditService auditService;
public OrderService(
EmailService emailService,
InventoryService inventoryService,
AuditService auditService) {
this.emailService = emailService;
this.inventoryService = inventoryService;
this.auditService = auditService;
}
public void placeOrder(Long orderId) {
inventoryService.reserve(orderId);
emailService.sendConfirmation(orderId);
auditService.record("Order placed: " + orderId);
}
}
With events:
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public OrderService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void placeOrder(Long orderId) {
eventPublisher.publishEvent(new OrderPlacedEvent(orderId));
}
}
The order service no longer needs to know all follow-up actions.
4. Creating an Event
Modern Spring can publish any object as an event.
public record OrderPlacedEvent(Long orderId) {
}
Older style:
public class OrderPlacedEvent extends ApplicationEvent {
private final Long orderId;
public OrderPlacedEvent(Object source, Long orderId) {
super(source);
this.orderId = orderId;
}
public Long getOrderId() {
return orderId;
}
}
Certification note: extending ApplicationEvent is no longer mandatory.
5. Publishing an Event
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void placeOrder(Long orderId) {
System.out.println("Saving order " + orderId);
publisher.publishEvent(new OrderPlacedEvent(orderId));
}
}
6. Listening to Events With @EventListener
@Component
public class OrderEventListener {
@EventListener
public void handle(OrderPlacedEvent event) {
System.out.println("Order placed: " + event.orderId());
}
}
Spring detects listener methods and invokes them when a matching event is published.
7. Multiple Listeners
@Component
public class EmailListener {
@EventListener
public void sendEmail(OrderPlacedEvent event) {
System.out.println("Sending email for order " + event.orderId());
}
}
@Component
public class AuditListener {
@EventListener
public void audit(OrderPlacedEvent event) {
System.out.println("Auditing order " + event.orderId());
}
}
OrderPlacedEvent
|
+--> EmailListener
|
+--> AuditListener
8. Event Listener Ordering
Use @Order to control listener execution order.
@Component
public class InventoryListener {
@EventListener
@Order(1)
public void reserveStock(OrderPlacedEvent event) {
System.out.println("Reserving stock");
}
}
@Component
public class EmailListener {
@EventListener
@Order(2)
public void sendEmail(OrderPlacedEvent event) {
System.out.println("Sending email");
}
}
Lower order values run first.
9. Conditional Event Listeners
Use condition to run a listener only when a condition matches.
public record OrderPlacedEvent(Long orderId, BigDecimal amount) {
}
@Component
public class HighValueOrderListener {
@EventListener(condition = "#event.amount().compareTo(T(java.math.BigDecimal).valueOf(10000)) > 0")
public void handleHighValueOrder(OrderPlacedEvent event) {
System.out.println("High-value order: " + event.orderId());
}
}
The condition uses Spring Expression Language.
10. Synchronous Event Handling
By default, Spring event listeners run synchronously in the same thread as the publisher.
Thread: http-nio-8080-exec-1
OrderService.placeOrder()
|
v
publishEvent()
|
+--> EmailListener.handle()
|
+--> AuditListener.handle()
|
v
placeOrder() returns
Important consequences:
- A slow listener delays the publisher.
- Listener exceptions can affect the publishing flow.
- The same transaction context may still be active depending on where the event is published.
11. Asynchronous Event Handling
An event listener can be made asynchronous using @Async.
@Configuration
@EnableAsync
public class AsyncConfig {
}
@Component
public class EmailListener {
@Async
@EventListener
public void sendEmail(OrderPlacedEvent event) {
System.out.println("Sending email asynchronously");
}
}
Request Thread
|
v
publishEvent()
|
+--> submits listener to executor
|
v
publisher returns
Worker Thread
|
v
EmailListener.sendEmail()
12. Transactional Event Listeners
@TransactionalEventListener runs event logic based on transaction phase.
@Component
public class OrderEmailListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendEmail(OrderPlacedEvent event) {
System.out.println("Sending email after commit");
}
}
Common phases:
| Phase | Meaning |
|---|---|
BEFORE_COMMIT | Runs before transaction commit |
AFTER_COMMIT | Runs after successful commit |
AFTER_ROLLBACK | Runs after rollback |
AFTER_COMPLETION | Runs after commit or rollback |
Use AFTER_COMMIT for actions that should happen only after data is successfully saved.
13. Event Traps
| Trap | Correct Understanding |
|---|---|
| Spring events are always asynchronous. | They are synchronous by default. |
Event classes must extend ApplicationEvent. | Modern Spring allows plain objects. |
@EventListener always runs after transaction commit. | Use @TransactionalEventListener for transaction-aware behavior. |
| Listener order is random and cannot be controlled. | Use @Order. |
@Async works without configuration. | @EnableAsync is required. |
| Async listener exceptions are always returned to the publisher. | They occur on another thread and need async exception handling. |
14. Async Processing
Spring async processing allows a method to run in a separate thread.
Enable async support:
@Configuration
@EnableAsync
public class AsyncConfig {
}
Use @Async:
@Service
public class ReportService {
@Async
public void generateReport() {
System.out.println("Generating report on " + Thread.currentThread().getName());
}
}
15. Async Return Types
void
@Async
public void sendEmail(String email) {
System.out.println("Sending email to " + email);
}
Use for fire-and-forget tasks.
CompletableFuture
@Async
public CompletableFuture<String> calculateScore(Long userId) {
String score = "Score for user " + userId;
return CompletableFuture.completedFuture(score);
}
Caller:
CompletableFuture<String> future = scoreService.calculateScore(10L);
future.thenAccept(System.out::println);
Future
@Async
public Future<String> process() {
return new AsyncResult<>("done");
}
CompletableFuture is generally preferred in modern applications.
16. Important @Async Rules
Rule 1: Method Must Be Called Through Spring Proxy
This does not work as expected:
@Service
public class ReportService {
public void start() {
generateReport();
}
@Async
public void generateReport() {
System.out.println("Async?");
}
}
The call is internal self-invocation, so it bypasses the Spring proxy.
Working version:
@Service
public class ReportFacade {
private final ReportService reportService;
public ReportFacade(ReportService reportService) {
this.reportService = reportService;
}
public void start() {
reportService.generateReport();
}
}
@Service
public class ReportService {
@Async
public void generateReport() {
System.out.println("Running asynchronously");
}
}
Rule 2: Method Should Usually Be Public
Because Spring commonly applies async behavior through proxies, public methods are the safest choice.
Rule 3: Exceptions Need Special Handling
For CompletableFuture, handle exceptions through the future.
scoreService.calculateScore(10L)
.exceptionally(ex -> "fallback");
For void async methods, configure AsyncUncaughtExceptionHandler.
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
System.err.println("Async error in " + method.getName() + ": " + ex.getMessage());
}
}
17. Executors
An executor manages threads used for async work.
Without custom configuration, Spring uses a default executor. In production, define your own.
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "applicationTaskExecutor")
public Executor applicationTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("app-async-");
executor.initialize();
return executor;
}
}
Use a named executor:
@Async("applicationTaskExecutor")
public CompletableFuture<String> generateInvoice(Long orderId) {
return CompletableFuture.completedFuture("invoice-" + orderId);
}
18. Executor Configuration Meaning
| Property | Meaning |
|---|---|
corePoolSize | Number of threads kept in the pool |
maxPoolSize | Maximum number of threads |
queueCapacity | Number of tasks that can wait before new threads are added or tasks are rejected |
threadNamePrefix | Prefix for worker thread names |
rejectedExecutionHandler | Strategy when pool and queue are full |
Incoming tasks
|
v
Core threads available? run task
|
v
Queue has space? enqueue task
|
v
Can grow to maxPoolSize? create more thread
|
v
Reject task
19. Rejection Policies
Common Java executor rejection policies:
| Policy | Behavior |
|---|---|
AbortPolicy | Throws RejectedExecutionException |
CallerRunsPolicy | Caller thread runs the task |
DiscardPolicy | Silently discards task |
DiscardOldestPolicy | Discards oldest queued task |
Prefer explicit rejection handling. Avoid silent discarding for important business tasks.
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
20. Scheduling
Spring scheduling runs methods periodically or according to a cron expression.
Enable scheduling:
@Configuration
@EnableScheduling
public class SchedulingConfig {
}
Scheduled method:
@Component
public class CleanupJob {
@Scheduled(fixedRate = 60000)
public void clean() {
System.out.println("Cleaning temporary files");
}
}
21. Fixed Rate vs Fixed Delay
fixedRate
Runs based on the start time of the previous execution.
@Scheduled(fixedRate = 5000)
public void runEveryFiveSeconds() {
}
Start 0s
Start 5s
Start 10s
Start 15s
fixedDelay
Runs after the previous execution finishes plus the delay.
@Scheduled(fixedDelay = 5000)
public void runFiveSecondsAfterCompletion() {
}
Start 0s
Finish 3s
Wait 5s
Start 8s
initialDelay
Delays the first execution.
@Scheduled(initialDelay = 10000, fixedRate = 60000)
public void runAfterStartupDelay() {
}
22. Cron Scheduling
@Scheduled(cron = "0 0 9 * * MON-FRI")
public void runEveryWeekdayAtNine() {
System.out.println("Running weekday job at 9 AM");
}
Spring cron format has six fields:
second minute hour day-of-month month day-of-week
Example:
| Cron | Meaning |
|---|---|
0 0 * * * * | Every hour |
0 */15 * * * * | Every 15 minutes |
0 0 0 * * * | Every day at midnight |
0 0 9 * * MON-FRI | Weekdays at 9 AM |
Use timezone:
@Scheduled(cron = "0 0 9 * * *", zone = "Asia/Kolkata")
public void runAtNineInIndia() {
}
23. Scheduling Rules and Traps
| Trap | Correct Understanding |
|---|---|
@Scheduled works automatically. | @EnableScheduling is required. |
| Scheduled methods can have any parameters. | Scheduled methods should generally be no-argument methods. |
fixedRate waits after completion. | fixedRate is based on start times. |
fixedDelay is based on start times. | fixedDelay waits after completion. |
| Spring cron has five fields like Unix cron. | Spring cron commonly uses six fields including seconds. |
| A long scheduled task is always parallelized. | It depends on scheduler configuration. |
24. Custom Scheduler Executor
For scheduled tasks, configure a task scheduler.
@Configuration
@EnableScheduling
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("scheduled-");
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
}
This allows multiple scheduled tasks to run concurrently.
25. Practical Example: Order Flow
Requirement
When an order is placed:
- Save the order.
- Publish an event.
- Send confirmation email asynchronously after transaction commit.
- Run a scheduled cleanup job for old orders.
Event
public record OrderPlacedEvent(Long orderId, String customerEmail) {
}
Order Service
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
private final OrderRepository orderRepository;
public OrderService(ApplicationEventPublisher publisher, OrderRepository orderRepository) {
this.publisher = publisher;
this.orderRepository = orderRepository;
}
@Transactional
public void placeOrder(Order order) {
Order savedOrder = orderRepository.save(order);
publisher.publishEvent(new OrderPlacedEvent(savedOrder.getId(), savedOrder.getCustomerEmail()));
}
}
Async Configuration
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "emailExecutor")
public Executor emailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("email-");
executor.initialize();
return executor;
}
}
Event Listener
@Component
public class OrderEmailListener {
private final EmailService emailService;
public OrderEmailListener(EmailService emailService) {
this.emailService = emailService;
}
@Async("emailExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendConfirmation(OrderPlacedEvent event) {
emailService.send(event.customerEmail(), "Order confirmed: " + event.orderId());
}
}
Scheduled Cleanup
@Component
public class OrderCleanupJob {
private final OrderRepository orderRepository;
public OrderCleanupJob(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Scheduled(cron = "0 0 2 * * *", zone = "Asia/Kolkata")
public void deleteOldDraftOrders() {
orderRepository.deleteDraftOrdersOlderThan(Duration.ofDays(30));
}
}
26. When to Use Events, Async, or Scheduling
| Need | Use |
|---|---|
| Notify other components about something that happened | Spring events |
| Run work outside the request thread | @Async |
| Run work after transaction commit | @TransactionalEventListener |
| Run periodic jobs | @Scheduled |
| Control thread count and queueing | Custom executor or scheduler |
27. Best Practices
- Use events for decoupling, not for hiding core business flow.
- Keep event payloads simple and meaningful.
- Prefer immutable event objects such as records.
- Use
@TransactionalEventListener(AFTER_COMMIT)for external side effects after database changes. - Configure explicit executors for production async workloads.
- Use meaningful thread name prefixes.
- Do not use async processing for work that must complete before returning a response.
- Monitor executor queue size, active thread count, and rejected tasks.
- Avoid silent rejection policies for important work.
- Keep scheduled jobs idempotent where possible.
- Use distributed locks for scheduled jobs in multi-instance deployments when only one instance should run the job.
28. Interview Questions and Answers
1. What are Spring events?
Spring events are messages published inside the application context to notify listeners that something happened.
2. Are Spring events synchronous or asynchronous?
They are synchronous by default. They can be made asynchronous with @Async and async configuration.
3. Does an event class need to extend ApplicationEvent?
No. Modern Spring allows publishing plain objects as events.
4. What is ApplicationEventPublisher?
It is the Spring interface used to publish events to the application context.
5. What is the difference between @EventListener and @TransactionalEventListener?
@EventListener reacts when the event is published. @TransactionalEventListener reacts according to a transaction phase such as after commit or after rollback.
6. Why use AFTER_COMMIT?
Use AFTER_COMMIT when an action should happen only after database changes are successfully committed, such as sending confirmation email.
7. What does @Async do?
It allows a Spring bean method to execute in a separate thread through a Spring proxy.
8. Why might @Async not work?
Common reasons include missing @EnableAsync, self-invocation, non-Spring-managed objects, or calling a method that is not proxied.
9. What return types are common for async methods?
Common return types are void, Future, and CompletableFuture.
10. How are exceptions handled in async methods?
For CompletableFuture, exceptions are handled through the future. For void async methods, use AsyncUncaughtExceptionHandler.
11. What is an executor?
An executor manages threads and task execution. In Spring, executors are commonly used for async methods.
12. What is the difference between fixedRate and fixedDelay?
fixedRate schedules based on start times. fixedDelay waits until the previous execution finishes, then waits the delay duration.
13. What enables scheduled tasks?
@EnableScheduling enables processing of @Scheduled methods.
14. How many fields does a Spring cron expression usually have?
Spring cron expressions commonly have six fields, including seconds.
15. What problem can occur with scheduled jobs in multiple application instances?
The same job may run on every instance. Use a distributed lock or external scheduler if only one execution is desired.
29. Quick Revision Notes
- Spring events decouple publishers and listeners.
- Use
ApplicationEventPublisherto publish events. - Use
@EventListenerto listen for events. - Spring events are synchronous by default.
- Add
@Asyncto make listener execution asynchronous. @EnableAsyncis required for@Async.@TransactionalEventListener(AFTER_COMMIT)is useful for post-commit side effects.@Ordercontrols listener order.@Asyncrequires proxy-based invocation.- Self-invocation does not trigger async behavior.
- Prefer
CompletableFuturefor async results. - Configure custom executors for production.
@EnableSchedulingis required for@Scheduled.fixedRateis based on start time.fixedDelaywaits after completion.- Spring cron usually includes seconds.
- Configure scheduler pools for concurrent scheduled tasks.
30. One-Page Cheat Sheet
Core Annotations
| Annotation | Purpose |
|---|---|
@EventListener | Handles application events |
@TransactionalEventListener | Handles events based on transaction phase |
@Async | Runs method asynchronously |
@EnableAsync | Enables async method execution |
@Scheduled | Runs method on schedule |
@EnableScheduling | Enables scheduled task execution |
@Order | Controls listener order |
Event Flow
Service publishes event
|
v
ApplicationEventPublisher
|
v
ApplicationEventMulticaster
|
+--> @EventListener method
+--> @TransactionalEventListener method
Async Checklist
| Question | Required Answer |
|---|---|
Is @EnableAsync present? | Yes |
| Is the class a Spring bean? | Yes |
| Is the method called through proxy? | Yes |
| Is the executor configured? | Preferably yes |
| Are exceptions handled? | Yes |
Scheduling Comparison
| Schedule Type | Behavior |
|---|---|
fixedRate | Runs based on previous start time |
fixedDelay | Runs after previous completion plus delay |
initialDelay | Delays first execution |
cron | Runs using cron expression |
Executor Sizing Terms
| Term | Meaning |
|---|---|
| Core pool size | Normal number of worker threads |
| Max pool size | Upper limit of worker threads |
| Queue capacity | Waiting room for pending tasks |
| Rejection policy | What happens when pool and queue are full |
Must-Remember Traps
- Events are synchronous unless explicitly made async.
@Asyncdoes not work on self-invocation.@EnableAsyncand@EnableSchedulingare required.@TransactionalEventListeneris different from@EventListener.- Spring cron commonly has six fields.
fixedRateandfixedDelayare not the same.- Production async workloads need explicit executors.
- Scheduled jobs may run on every application instance.