Appearance
Observer Pattern
Introduction
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. It is the foundational pattern behind event-driven systems, reactive streams, and publish-subscribe architectures. Without it, a subject that needs to inform other components of state changes must maintain direct references to every interested party — an arrangement that collapses under its own coupling as the system grows.
Intent
Define a subscription mechanism that allows multiple observer objects to be notified automatically when a subject object changes state, without the subject knowing the concrete types of its observers.
Structure
Participants
| Participant | Role |
|---|---|
Subject (Subject interface) | Maintains the list of observers and defines subscribe/unsubscribe operations. |
ConcreteSubject (OrderEventPublisher) | Holds state and fires notification events when state changes. |
Observer (OrderEventListener interface) | Declares the update interface that all observers must implement. |
ConcreteObservers (EmailNotificationListener, InventoryListener, AnalyticsListener) | React to notifications and perform their specific side effects. |
Event (OrderEvent) | Carries the data observers need; used in the push model. |
Anti-Pattern — Hardcoded Side-Effect Chain
The naive approach has the Order entity call each downstream service directly. Every new reaction to an order status change requires modifying the Order class itself — a clear violation of the Open/Closed Principle and the Single Responsibility Principle.
java
// ANTI-PATTERN: Order knows about every downstream system
public class Order {
private String orderId;
private String status;
private final EmailService emailService;
private final InventoryService inventoryService;
private final AnalyticsService analyticsService;
public Order(String orderId,
EmailService emailService,
InventoryService inventoryService,
AnalyticsService analyticsService) {
this.orderId = orderId;
this.emailService = emailService;
this.inventoryService = inventoryService;
this.analyticsService = analyticsService;
this.status = "PENDING";
}
public void updateStatus(String newStatus) {
String previous = this.status;
this.status = newStatus;
// Direct coupling to every interested party:
emailService.sendStatusUpdate(orderId, newStatus); // ← must modify Order to add/remove
inventoryService.onOrderStatusChanged(orderId, newStatus); // ← must modify Order to add/remove
analyticsService.trackStatusTransition(orderId, previous, newStatus); // ← same
// Adding a PushNotificationService means opening this class again.
}
}Problems:
Orderis coupled to every system that cares about its state. Adding a fourth service requires reopening and re-testingOrder.Orderis untestable in isolation — constructing it requires all three service implementations.- Removing a listener at runtime is impossible without structural code changes.
Correct Implementation
Event and Listener Contracts
java
import java.time.Instant;
// Immutable event carries all context observers need (push model)
public record OrderEvent(
String orderId,
String customerId,
String previousStatus,
String newStatus,
Instant occurredAt
) {}java
// Observer contract — decoupled from the subject
@FunctionalInterface
public interface OrderEventListener {
void onOrderEvent(OrderEvent event);
}Publisher (ConcreteSubject)
java
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class OrderEventPublisher {
// CopyOnWriteArrayList is safe for concurrent iteration while add/remove happen
private final List<OrderEventListener> listeners = new CopyOnWriteArrayList<>();
private final String orderId;
private final String customerId;
private String status;
public OrderEventPublisher(String orderId, String customerId) {
this.orderId = orderId;
this.customerId = customerId;
this.status = "PENDING";
}
public void subscribe(OrderEventListener listener) {
listeners.add(listener);
}
public void unsubscribe(OrderEventListener listener) {
listeners.remove(listener);
}
public void updateStatus(String newStatus) {
String previous = this.status;
this.status = newStatus;
OrderEvent event = new OrderEvent(
orderId, customerId, previous, newStatus, Instant.now()
);
notifyListeners(event);
}
private void notifyListeners(OrderEvent event) {
for (OrderEventListener listener : listeners) {
try {
listener.onOrderEvent(event);
} catch (Exception ex) {
// Isolate failures: one bad listener must not suppress others
System.err.printf("Listener %s failed for event %s: %s%n",
listener.getClass().getSimpleName(), event.orderId(), ex.getMessage());
}
}
}
}Concrete Observers
java
public class EmailNotificationListener implements OrderEventListener {
private final EmailService emailService;
public EmailNotificationListener(EmailService emailService) {
this.emailService = emailService;
}
@Override
public void onOrderEvent(OrderEvent event) {
if ("SHIPPED".equals(event.newStatus())) {
emailService.sendShipmentConfirmation(event.customerId(), event.orderId());
} else if ("CANCELLED".equals(event.newStatus())) {
emailService.sendCancellationNotice(event.customerId(), event.orderId());
}
}
}java
public class InventoryListener implements OrderEventListener {
private final InventoryService inventoryService;
public InventoryListener(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
@Override
public void onOrderEvent(OrderEvent event) {
if ("CONFIRMED".equals(event.newStatus())) {
inventoryService.reserveItems(event.orderId());
} else if ("CANCELLED".equals(event.newStatus())) {
inventoryService.releaseItems(event.orderId());
}
}
}java
public class AnalyticsListener implements OrderEventListener {
private final AnalyticsService analyticsService;
public AnalyticsListener(AnalyticsService analyticsService) {
this.analyticsService = analyticsService;
}
@Override
public void onOrderEvent(OrderEvent event) {
analyticsService.trackTransition(
event.orderId(), event.previousStatus(), event.newStatus(), event.occurredAt()
);
}
}Wiring and Usage
java
public class ObserverDemo {
public static void main(String[] args) {
OrderEventPublisher order = new OrderEventPublisher("ORD-99", "CUST-7");
// Register observers — Order itself never changes when we add more
order.subscribe(new EmailNotificationListener(new EmailService()));
order.subscribe(new InventoryListener(new InventoryService()));
order.subscribe(new AnalyticsListener(new AnalyticsService()));
// Adding a push notification observer requires zero modification to Order
order.subscribe(event -> System.out.printf(
"[Push] Order %s is now %s%n", event.orderId(), event.newStatus()
));
order.updateStatus("CONFIRMED");
order.updateStatus("SHIPPED");
}
}Push Model vs. Pull Model
The implementation above uses the push model: the subject packages all relevant data into the event and forwards it to each observer. The pull model sends a minimal notification and lets the observer call back to the subject to retrieve what it needs.
| Push Model | Pull Model | |
|---|---|---|
| Coupling | Observer depends on event structure | Observer depends on subject query API |
| Latency | Lower — one trip | Higher — two trips |
| Selectivity | Observer receives everything | Observer fetches only what it needs |
| Stale data | None | Possible between notification and fetch |
Prefer push when the event data is small and most observers need all of it. Prefer pull when observers need highly selective slices of a large subject state.
Pitfall — Memory Leaks from Underegistered Observers
If observers are added but never removed, the subject holds a strong reference to them and prevents garbage collection. This is especially dangerous when observers have short lifetimes (e.g., UI components, request-scoped beans).
java
// PITFALL: this listener is registered but never removed
// The OrderEventPublisher will hold it alive indefinitely
OrderEventListener temporaryListener = event -> System.out.println("Temporary: " + event.orderId());
order.subscribe(temporaryListener);
// ... temporaryListener goes out of scope in calling code, but order still holds the referenceMitigation — Weak References
java
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
public class WeakReferencePublisher {
private final List<WeakReference<OrderEventListener>> listeners = new CopyOnWriteArrayList<>();
public void subscribe(OrderEventListener listener) {
listeners.add(new WeakReference<>(listener));
}
private void notifyListeners(OrderEvent event) {
listeners.removeIf(ref -> {
OrderEventListener listener = ref.get();
if (listener == null) {
return true; // GC has collected it — prune the dead reference
}
listener.onOrderEvent(event);
return false;
});
}
}The trade-off: with weak references the caller must hold a strong reference to the listener for as long as it wants to receive events — otherwise the GC may collect it between notifications. Explicit unsubscribe calls are almost always cleaner and more predictable.
Real-World Examples
java.util.Observer (deprecated since Java 9)
java.util.Observer and java.util.Observable were the original JDK incarnation. They were deprecated because Observable is a class (preventing other inheritance), its notify mechanism is not thread-safe, and there is no event type — observers always receive a raw Object. The interface-based approach shown above avoids all three problems.
PropertyChangeListener
java.beans.PropertyChangeListener is the JDK's typed observer for JavaBeans. It uses a PropertyChangeEvent carrying the property name, old value, and new value — a push model with structured metadata.
java
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
public class OrderBean {
private final PropertyChangeSupport support = new PropertyChangeSupport(this);
private String status = "PENDING";
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
public void setStatus(String newStatus) {
String old = this.status;
this.status = newStatus;
support.firePropertyChange("status", old, newStatus);
}
}Spring ApplicationEvent / ApplicationListener
Spring's event infrastructure is Observer backed by the application context. Publishing an event decouples the producer from all listeners registered in the context.
java
// Event
public class OrderStatusChangedEvent extends ApplicationEvent {
private final OrderEvent orderEvent;
public OrderStatusChangedEvent(Object source, OrderEvent orderEvent) {
super(source);
this.orderEvent = orderEvent;
}
public OrderEvent getOrderEvent() { return orderEvent; }
}
// Publisher
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void updateStatus(String orderId, String newStatus) {
// ... persist status change ...
publisher.publishEvent(new OrderStatusChangedEvent(this,
new OrderEvent(orderId, "CUST-1", "PENDING", newStatus, Instant.now())
));
}
}
// Listener — registered automatically by Spring
@Component
public class AuditListener implements ApplicationListener<OrderStatusChangedEvent> {
@Override
public void onApplicationEvent(OrderStatusChangedEvent event) {
System.out.println("Auditing: " + event.getOrderEvent());
}
}AWS SNS Fan-Out
AWS SNS is Observer at the infrastructure level. A single Publish call to an SNS topic delivers the message to all subscribed SQS queues, Lambda functions, HTTP endpoints, and email addresses. Adding a new subscriber requires no change to the publishing service.
Reactive Streams — Publisher / Subscriber
The Reactive Streams spec (java.util.concurrent.Flow.Publisher / Flow.Subscriber) is Observer with back-pressure. The subscriber controls the rate of notifications by calling subscription.request(n), preventing a fast subject from overwhelming a slow observer.
java
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
public class ReactiveObserverDemo {
public static void main(String[] args) throws InterruptedException {
SubmissionPublisher<OrderEvent> publisher = new SubmissionPublisher<>();
publisher.subscribe(new Flow.Subscriber<>() {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription sub) {
this.subscription = sub;
sub.request(Long.MAX_VALUE); // request all items (unbounded)
}
@Override
public void onNext(OrderEvent event) {
System.out.println("Reactive observer received: " + event.orderId());
subscription.request(1);
}
@Override public void onError(Throwable t) { t.printStackTrace(); }
@Override public void onComplete() { System.out.println("Stream complete"); }
});
publisher.submit(new OrderEvent("ORD-1", "CUST-1", "PENDING", "CONFIRMED", Instant.now()));
Thread.sleep(100);
publisher.close();
}
}When to Use
- A change in one object requires updating an unknown or variable number of other objects, and you do not want those objects tightly coupled to the subject.
- An object should notify other objects without assumptions about who those objects are.
- You need to support broadcast communication — one event, many recipients.
- You are implementing an event-driven or reactive architecture.
Consequences
Benefits
- Decouples subjects from observers. The subject publishes events; it has no knowledge of what consumes them.
- Supports the Open/Closed Principle: new observers can be added without modifying the subject.
- Enables runtime composition of behaviors — observers can be added and removed dynamically.
- Maps naturally to event-driven infrastructure (SNS, Kafka, Spring events).
Trade-offs
- Unexpected updates: a single state change can cascade through many observers in a hard-to-predict order.
- Update storms: if observers themselves trigger further state changes on the subject, you can enter an infinite notification loop.
- Memory leaks: underegistered observers keep the subject — and themselves — alive indefinitely (see Weak Reference mitigation above).
- Ordering is not guaranteed: unless you impose it explicitly, observers fire in registration order, which is an implementation detail callers should not rely on.
- Debugging difficulty: the indirect notification path makes it harder to trace which observer reacted to which event.
Related Concepts
- Saga Pattern: Choreography-based sagas are built on Observer — each service subscribes to events from other services and reacts to drive the distributed transaction forward.
- Strategy Pattern: Where Observer decouples who reacts to an event, Strategy decouples how an algorithm executes. The two are often combined: an event triggers the selection of a strategy.
- Template Method Pattern: Template Method defines a fixed processing skeleton; observer hooks can be inserted at defined steps of that skeleton.
- Open/Closed Principle: Observer is a primary mechanism for achieving OCP — the subject is closed for modification even as new observers extend its behavioral surface.
- Clean Architecture: In Clean Architecture, domain events are how use cases communicate across layer boundaries without coupling inner layers to outer ones.