Introduction to the Observer Pattern
In a modern backend system, you often need to perform multiple actions when a single event occurs. For example, when a user completes a purchase:
- Update the Inventory.
- Send an Email Receipt.
- Notify the Shipping Service.
- Update the Loyalty Points.
If you put all this logic into a single OrderService, you violate the Single Responsibility Principle (SRP). The code becomes a "God Class" that is hard to test and maintain.
The Observer Pattern allows a "Subject" to notify a list of "Observers" (listeners) automatically when its state changes, without knowing who they are.
1. The Classic Java Implementation
The basic pattern consists of a Subject interface and an Observer interface.
public interface Observer {
void update(String event);
}
public class EmailNotificationObserver implements Observer {
@Override
public void update(String event) {
System.out.println("Sending Email: " + event);
}
}
public class Subject {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void notifyObservers(String event) {
for (Observer observer : observers) {
observer.update(event);
}
}
}
The Problem with the Classic Approach
- Thread Safety: What if multiple threads add/remove observers or trigger notifications simultaneously?
- Tight Coupling: You still have to manually register observers.
- Synchronous Execution: If one observer is slow (e.g., sending an email), the entire Subject is blocked.
2. Production Pattern: Spring @EventListener
In a real Spring Boot production system, we rarely implement the interfaces manually. We use Application Events.
Step 1: Define the Event
public class OrderPlacedEvent {
private final String orderId;
// Constructor and Getter
}
Step 2: Publish the Event
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void placeOrder(String orderId) {
// Business logic...
eventPublisher.publishEvent(new OrderPlacedEvent(orderId));
}
}
Step 3: Listen to the Event
@Component
public class InventoryListener {
@EventListener
public void handleOrder(OrderPlacedEvent event) {
System.out.println("Updating inventory for: " + event.getOrderId());
}
}
@Component
public class EmailListener {
@EventListener
@Async // Make it non-blocking!
public void handleOrder(OrderPlacedEvent event) {
// Long running email task
}
}
3. Senior Level Considerations
A. Asynchronicity (@Async)
By default, Spring events are synchronous. If an observer fails, the original transaction might roll back. Use @Async to ensure that failure in a non-critical observer (like analytics) doesn't crash the main business process.
B. Transactional Bound Events (@TransactionalEventListener)
What if the database transaction fails after you've sent the "Order Successful" email?
Use @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) to ensure observers only run after the database has successfully saved the data.
C. Error Handling
Always wrap your observer logic in a try-catch block. An unhandled exception in an observer can block the thread or, if synchronous, prevent the main task from completing.
When to use Observer vs. Message Queues (Kafka)?
- Use Observer (Local Events): For logic within the same microservice. It's faster, has zero network overhead, and is easier to manage.
- Use Kafka/RabbitMQ: For communication between different microservices.
Final Takeaways
- The Observer pattern is the foundation of Decoupled Architecture.
- Use Spring's built-in event system instead of manual implementations.
- Always think about Transactional boundaries and Error handling in observers.