Spring: Application Events & @EventListener
Spring's event system provides a way to decouple components using the observer pattern. One component publishes an event and other components listen for it without any direct dependency between them. This is useful for cross-cutting concerns like sending notifications, logging, or triggering downstream processes after an action completes.
Contents
ApplicationEvent and ApplicationEventPublisher
Create a custom event class (it does not need to extend ApplicationEventApplicationEventPublisher
// Custom event (plain POJO — no need to extend ApplicationEvent)
public class OrderPlacedEvent {
private final Long orderId;
private final String customerEmail;
public OrderPlacedEvent(Long orderId, String customerEmail) {
this.orderId = orderId;
this.customerEmail = customerEmail;
}
public Long getOrderId() { return orderId; }
public String getCustomerEmail() { return customerEmail; }
}
// Publisher — inject ApplicationEventPublisher
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final OrderRepository orderRepo;
private final ApplicationEventPublisher eventPublisher;
public OrderService(OrderRepository orderRepo, ApplicationEventPublisher eventPublisher) {
this.orderRepo = orderRepo;
this.eventPublisher = eventPublisher;
}
public Order placeOrder(OrderRequest request) {
Order order = orderRepo.save(new Order(request));
// Publish event after saving — listeners will be notified
eventPublisher.publishEvent(new OrderPlacedEvent(order.getId(), request.getEmail()));
return order;
}
}
@EventListener
Annotate any Spring-managed bean method with @EventListener
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class OrderNotificationListener {
private final EmailService emailService;
public OrderNotificationListener(EmailService emailService) {
this.emailService = emailService;
}
@EventListener
public void onOrderPlaced(OrderPlacedEvent event) {
emailService.sendConfirmation(event.getCustomerEmail(), event.getOrderId());
}
}
@Component
public class AuditListener {
private final AuditRepository auditRepo;
public AuditListener(AuditRepository auditRepo) {
this.auditRepo = auditRepo;
}
@EventListener
public void onOrderPlaced(OrderPlacedEvent event) {
auditRepo.log("ORDER_PLACED", event.getOrderId().toString());
}
}
@EventListenerAsync Events with @Async
Add @Async
// Enable async processing — put this on a @Configuration class
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(4);
exec.setMaxPoolSize(10);
exec.setQueueCapacity(100);
exec.setThreadNamePrefix("event-");
exec.initialize();
return exec;
}
}
@Component
public class OrderEmailListener {
@Async // runs on the event- thread pool
@EventListener
public void onOrderPlaced(OrderPlacedEvent event) {
// this runs asynchronously — the publisher does not wait
emailService.sendConfirmation(event.getCustomerEmail(), event.getOrderId());
}
}
AsyncUncaughtExceptionHandler@EventListener@AsyncConditional Listeners with condition
Use the conditiontrue
@Component
public class HighValueOrderListener {
// Only process orders above £500
@EventListener(condition = "#event.amount > 500")
public void onHighValueOrder(OrderPlacedEvent event) {
fraudDetectionService.review(event.getOrderId());
}
// Only process events for a specific customer tier
@EventListener(condition = "#event.customerTier == 'PREMIUM'")
public void onPremiumOrder(OrderPlacedEvent event) {
priorityQueueService.enqueue(event.getOrderId());
}
}
Built-in Spring Events
| Event | When fired | Use case |
|---|---|---|
ContextRefreshedEvent | ApplicationContext is initialised or refreshed | Warm up caches, validate configuration |
ContextStartedEvent | After context.start() | Start scheduled jobs |
ContextStoppedEvent | After context.stop() | Stop scheduled jobs |
ContextClosedEvent | ApplicationContext is closed | Release resources |
ApplicationReadyEvent | Application is ready to serve requests (Spring Boot) | Startup tasks after full boot |
ApplicationStartedEvent | After context refresh but before command-line runners | Observability initialisation |
@Component
public class StartupListener {
private final CacheService cacheService;
public StartupListener(CacheService cacheService) {
this.cacheService = cacheService;
}
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
cacheService.warmUp();
System.out.println("Application is ready — cache warmed up");
}
}
Ordering with @Order
When multiple listeners handle the same event, use @Order
@Component
@Order(1) // runs first
public class ValidationListener {
@EventListener
public void onOrder(OrderPlacedEvent event) {
validator.validate(event.getOrderId());
}
}
@Component
@Order(2) // runs second
public class InventoryListener {
@EventListener
public void onOrder(OrderPlacedEvent event) {
inventory.reserve(event.getOrderId());
}
}
@Component
@Order(3) // runs last
public class NotificationListener {
@EventListener
public void onOrder(OrderPlacedEvent event) {
emailService.notify(event.getCustomerEmail());
}
}
@Async