The Spring Event system is often misunderstood as a simple observer pattern, but its true power lies in its ability to decouple components so thoroughly that they don’t even need to know about each other’s existence beyond a shared event contract.
Let’s see this in action. Imagine we have a UserRegistrationService that needs to send a welcome email and update a user analytics system after a new user signs up. Instead of injecting EmailService and AnalyticsService directly into UserRegistrationService, we’ll use the ApplicationEventPublisher.
First, define the event:
public class UserRegisteredEvent extends ApplicationEvent {
private final User user;
public UserRegisteredEvent(Object source, User user) {
super(source);
this.user = user;
}
public User getUser() {
return user;
}
}
Next, the service that publishes the event:
@Service
public class UserRegistrationService {
private final ApplicationEventPublisher eventPublisher;
public UserRegistrationService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void registerUser(User user) {
// ... registration logic ...
System.out.println("User registered: " + user.getEmail());
eventPublisher.publishEvent(new UserRegisteredEvent(this, user));
}
}
Now, the listeners that react to the event. Notice they don’t know about UserRegistrationService, only about UserRegisteredEvent.
@Component
public class WelcomeEmailListener {
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
User user = event.getUser();
System.out.println("Sending welcome email to: " + user.getEmail());
// ... email sending logic ...
}
}
@Component
public class UserAnalyticsListener {
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
User user = event.getUser();
System.out.println("Updating analytics for user: " + user.getEmail());
// ... analytics update logic ...
}
}
When UserRegistrationService.registerUser() is called, eventPublisher.publishEvent() is invoked. Spring’s ApplicationEventMulticaster (which is automatically configured) finds all beans that have methods annotated with @EventListener matching the UserRegisteredEvent type and calls them.
The core problem Spring Events solve is runtime decoupling. Instead of a direct A -> B call, you have A -> Event -> B and A -> Event -> C. A doesn’t know B or C exist. B and C only know about the Event. This makes systems much more flexible. You can add a new listener (e.g., AuditLogListener) that also reacts to UserRegisteredEvent without changing UserRegistrationService or the existing listeners at all. Spring handles the wiring.
The ApplicationEventPublisher is an interface, and its default implementation is usually SpringApplicationEventMulticaster. This multicaster is responsible for broadcasting events to all registered listeners. The listeners themselves are discovered by Spring’s component scanning and then registered with the multicaster when their @EventListener methods are found.
The ApplicationEvent class is a simple, generic event class. You extend it to carry your specific data. The source parameter in the constructor is conventionally set to the object that published the event, which can be useful for listeners if they need to know who sent the event, though often it’s not strictly necessary.
When an event is published, the ApplicationEventMulticaster iterates through all registered listener methods. If a listener method’s parameter type is assignable from the published event’s type, that method is invoked. This is how Spring achieves type-safe listening. For instance, if you publish a UserRegisteredEvent, only methods expecting UserRegisteredEvent (or a superclass like ApplicationEvent) will be called.
A subtle but powerful aspect is that listeners are invoked synchronously and in the order they are registered (or more accurately, based on their declaration order or ordering annotations like @Order). This means if WelcomeEmailListener throws an exception, UserAnalyticsListener (and any subsequent listeners) will not be executed. This synchronous nature is crucial for maintaining transactional integrity if the event publishing is within a transaction; Spring ensures that if the transaction rolls back, the events that were published within it are also discarded, preventing partial state updates.
If you need asynchronous event handling, you can configure the @EventListener annotation with the classes attribute to specify the event type and the executor attribute to point to a TaskExecutor bean. This is essential for long-running listener tasks that could otherwise block the main thread and degrade application performance.
The next concept to explore is how to handle transactional events, ensuring events are only processed if the transaction in which they were published commits successfully.