Observer Pattern & Events
The observer pattern lets an object (the subject) notify many dependents (observers) of a change without knowing who they are. Spring implements it as its application events mechanism: a publisher fires an event, and any number of @EventListener beans react — none of them coupled to each other. This is the idiomatic way to decouple a side effect (sending a welcome email, updating a cache, writing an audit log) from the core action that triggers it.
Publisher and listeners, decoupled
In the GoF pattern, the subject keeps a list of observers and calls them. In Spring, the ApplicationContext is the registry of observers, so the publisher only needs to publish — it never holds references to listeners.
// 1. The event — an immutable record carrying the data observers need.
public record UserRegisteredEvent(Long userId, String email) { }
// 2. The publisher — fires the event, knows nothing about who listens.
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class RegistrationService {
private final ApplicationEventPublisher events;
private final UserRepository users;
public User register(String email, String password) {
User user = users.save(new User(email, password));
events.publishEvent(new UserRegisteredEvent(user.getId(), email));
return user;
}
}
// 3. The observers — each reacts independently; add or remove freely.
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class WelcomeEmailListener {
@EventListener
public void onRegistered(UserRegisteredEvent event) {
// send a welcome email to event.email()
}
}
@Component
public class AnalyticsListener {
@EventListener
public void onRegistered(UserRegisteredEvent event) {
// record a signup metric
}
}
RegistrationService publishes one event and two listeners run — with no compile-time dependency between them. Adding a third side effect means adding a third listener, leaving RegistrationService untouched. See application events for the full API.
Synchronous by default
By default, publishEvent runs every listener synchronously, on the publisher’s thread, inside the same transaction. If a listener throws, it propagates back to the publisher and can roll the transaction back. That is sometimes what you want and sometimes a trap.
Asynchronous listeners
To run a listener off the publishing thread, add @Async (and enable it with @EnableAsync). The publisher returns immediately and the listener runs on a separate thread:
import org.springframework.scheduling.annotation.Async;
@Component
public class WelcomeEmailListener {
@Async
@EventListener
public void onRegistered(UserRegisteredEvent event) {
// runs on the async executor; failures here do NOT affect the caller
}
}
| Synchronous (default) | @Async listener | |
|---|---|---|
| Thread | Publisher’s thread | Async executor thread |
| Transaction | Shares publisher’s tx | Own/no transaction |
| Failure impact | Can fail/roll back caller | Isolated from caller |
| Use for | Must complete before continuing | Fire-and-forget side effects |
Note:
@Asynclisteners use the proxy mechanism described in Proxy Pattern & AOP, so the same self-invocation and@EnableAsyncrules apply.
@TransactionalEventListener — fire after commit
A common bug: a synchronous listener sends an email, then the publisher’s transaction rolls back — and you have emailed a user about an account that no longer exists. @TransactionalEventListener defers the listener until a chosen transaction phase, by default AFTER_COMMIT:
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.event.TransactionPhase;
@Component
public class WelcomeEmailListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onRegistered(UserRegisteredEvent event) {
// runs only if the registration transaction actually committed
}
}
Now the email is sent only when the user is truly persisted. Combine with @Async to also move it off-thread.
Warning:
@TransactionalEventListenerdoes nothing useful if the event is published outside a transaction — with no transaction to bind to, the listener (by default) is skipped. Publish such events from within a@Transactionalmethod.
Tip: Keep events small, immutable, and named in the past tense (
UserRegisteredEvent,OrderShippedEvent). They describe something that already happened, which is exactly the observer pattern’s notification semantics.