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 ApplicationEvent since Spring 4.2: any POJO works) and inject ApplicationEventPublisher into the component that fires it.

Java
// 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; }
}
Java
// 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. Spring calls it automatically when the matching event type is published. The parameter type determines which event triggers the method.

Java
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());
    }
}
By default, @EventListener methods are called synchronously in the same thread as the publisher. The publisher's method does not return until all listeners have completed.

Async Events with @Async

Add @Async to a listener method to run it on a separate thread pool. This allows the publisher to return immediately without waiting for listeners to complete.

Java
// 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;
    }
}
Java
@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());
    }
}
Async listeners cannot return values or throw exceptions back to the publisher. Exceptions are handled by the configured AsyncUncaughtExceptionHandler. Use @EventListener without @Async when you need the publisher to react to listener failures.

Conditional Listeners with condition

Use the condition attribute with a Spring Expression Language (SpEL) expression to filter events. The listener is only invoked when the expression evaluates to true.

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

EventWhen firedUse case
ContextRefreshedEventApplicationContext is initialised or refreshedWarm up caches, validate configuration
ContextStartedEventAfter context.start() is calledStart scheduled jobs
ContextStoppedEventAfter context.stop() is calledStop scheduled jobs
ContextClosedEventApplicationContext is closedRelease resources
ApplicationReadyEventApplication is ready to serve requests (Spring Boot)Startup tasks after full boot
ApplicationStartedEventAfter context refresh but before command-line runnersObservability initialisation
Java
@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 to control the execution order. Lower values run first.

Java
@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());
    }
}
Ordering only applies to synchronous listeners. Async listeners (@Async) execute on different threads and their relative execution order cannot be guaranteed.