Observer pattern
The observer pattern decouples event producers from consumers. A subject notifies all registered observers of state changes without knowing who they are or what they do.
Introduction
The Observer pattern is a one-to-many notification pattern. A subject publishes a state change or event, and registered observers react without the subject knowing their concrete classes.
TL;DR / mental model
Think “publisher announces; listeners decide how to react.” Observer is commonly an in-process, synchronous collaboration unless you add asynchronous or durable infrastructure; it does not by itself provide a message queue’s delivery guarantees.
The Problem It Solves
Your OrderService places an order. Now it needs to send a confirmation email, update inventory, and fire an analytics event. The naive approach calls all three directly:
Three problems. First, OrderService violates Single Responsibility because it knows about email, inventory, and analytics. Second, adding a fourth reaction (loyalty points, fraud check) means opening OrderService and adding another dependency. Third, testing placeOrder requires mocking three unrelated services.
This anti-pattern appears when a single "god method" performs the primary action and then triggers many unrelated side effects inline. Observer can decouple those reactions from the primary action.
Here is what changes when you apply the Observer pattern.
Core idea
The subject maintains a list of observers and notifies them automatically. Observers register themselves. The subject never imports or instantiates any observer directly.
Structure
OrderEventPublisher is the subject. It holds a list of OrderEventListener instances and iterates through them when an event fires. Each listener implements one interface method. The publisher does not import EmailNotifier or InventoryUpdater directly, so adding a new listener can be a wiring-only change.
Implementation
// Immutable event object using a Java record.
// Records give us equals(), hashCode(), and toString() for free.
// We pass an event object instead of raw args so adding fields
// (like couponCode) can be added while keeping the listener parameter type stable.
public record OrderEvent(
String orderId,
String customerEmail,
List<String> itemIds,
double totalAmount,
Instant placedAt
) {}Thread-safe dispatch
CopyOnWriteArrayList is one practical choice when observer lists are read often and changed infrequently. It copies the backing array on every write (subscribe/unsubscribe), so each iteration uses a stable snapshot. The tradeoff is higher write cost and memory churn during subscription changes.
Async dispatch with ExecutorService
When a slow listener (email sending, HTTP analytics call) should not block the publisher thread, dispatch asynchronously:
public class AsyncOrderService {
private final List<OrderEventListener> listeners = new CopyOnWriteArrayList<>();
// Bounded thread pool prevents runaway thread creation.
private final ExecutorService executor = Executors.newFixedThreadPool(4);
public void placeOrder(Order order) {
Order saved = saveToDatabase(order);
OrderEvent event = buildEvent(saved);
// Each listener runs on its own thread.
// A slow EmailNotifier no longer blocks InventoryUpdater.
for (OrderEventListener listener : listeners) {
executor.submit(() -> {
try {
listener.onOrderPlaced(event);
} catch (Exception e) {
// Log and continue. One listener failure must not kill others.
log.error("Listener failed: {}", listener.getClass().getSimpleName(), e);
}
});
}
}
}
Catch exceptions per listener
If one observer throws and you do not catch it, the remaining observers in a synchronous loop will not run. When listeners are independent, isolate each listener call with a try/catch, whether dispatch is synchronous or asynchronous.
Error isolation strategy
When independent listeners should not block one another, the dispatch loop needs error isolation. One implementation looks like this:
// Resilient dispatch: log failures and continue with the other listeners.
private void notifyListeners(OrderEvent event) {
for (OrderEventListener listener : listeners) {
try {
listener.onOrderPlaced(event);
} catch (Exception e) {
// Log which listener failed and why. The policy may rethrow
// or enqueue the failure when the event is not best-effort.
log.error("Observer failed: {} for order {}",
listener.getClass().getSimpleName(),
event.orderId(), e);
// Optional: push to a dead letter queue for retry.
deadLetterQueue.enqueue(listener.getClass(), event, e);
}
}
}
Whether to isolate failures is a delivery policy, but it matters when observers are independent. Without isolation, a failing observer can prevent later listeners from receiving the event; with isolation, the publisher still needs a policy for logging, retrying, or surfacing that failure.
Typed events with generics
When your system has multiple event types (OrderPlaced, OrderShipped, OrderCancelled), a generic listener avoids writing a separate interface for each:
// Generic event listener. T is the event type.
@FunctionalInterface
public interface EventListener<T> {
void handle(T event);
}
// Type-safe publisher parameterized by event type.
public class EventPublisher<T> {
private final List<EventListener<T>> listeners = new CopyOnWriteArrayList<>();
public void subscribe(EventListener<T> listener) { listeners.add(listener); }
public void publish(T event) {
for (EventListener<T> listener : listeners) {
listener.handle(event);
}
}
}
// Usage: one publisher per event type, fully type-safe.
EventPublisher<OrderEvent> orderPublisher = new EventPublisher<>();
EventPublisher<ShipmentEvent> shipmentPublisher = new EventPublisher<>();
orderPublisher.subscribe(event -> sendEmail(event));
This generic shape is common in typed event APIs. Frameworks such as Spring's ApplicationEventPublisher, Guava's EventBus, and Reactor's Flux expose their own event abstractions and lifecycle policies.
Observer ordering and priority
By default, observers fire in subscription order. Some systems need priority ordering (security checks before logging, validation before persistence). Two approaches:
// Approach 1: Explicit priority via annotation or interface.
public interface PrioritizedListener extends OrderEventListener {
int priority(); // Lower = fires first.
}
// Approach 2: Separate listener lists per phase.
public class OrderService {
private final List<OrderEventListener> preListeners = new CopyOnWriteArrayList<>();
private final List<OrderEventListener> postListeners = new CopyOnWriteArrayList<>();
public void placeOrder(Order order) {
Order saved = saveToDatabase(order);
OrderEvent event = buildEvent(saved);
preListeners.forEach(l -> l.onOrderPlaced(event)); // validation, security
postListeners.forEach(l -> l.onOrderPlaced(event)); // email, analytics
}
}
Separate lists are often clearer than priority numbers. Priority numbers create hidden dependencies between observers ("analytics must be priority 5 because it runs after priority 3 validation"). Separate lists make the phases explicit.
How It Works
- The client calls
placeOrder()onOrderService. OrderServicepersists the order, then builds an immutableOrderEventrecord.- It iterates the listener list and calls
onOrderPlaced()on each one in subscription order. - Each listener processes the event independently.
EmailNotifiersends the confirmation,InventoryUpdaterdecrements stock,AnalyticsTrackerlogs metrics. - After all listeners return,
OrderServicereturns control to the client.
With async dispatch (the ExecutorService variant), step 3 submits each call to a thread pool instead of calling synchronously. The publisher returns to the client immediately after submitting, without waiting for listeners to finish.
Notice the immutable event object. The OrderEvent record is shared across all listeners without defensive copies because records in Java are immutable by design. This is critical for thread safety in the async variant: if the event were mutable, one listener could modify it while another is reading it.
Real-World Examples
Java Swing / AWT uses the Observer pattern everywhere. JButton.addActionListener(listener) registers an ActionListener that fires on click. The button (subject) knows nothing about what the listener does.
Spring ApplicationEventPublisher is a framework-level observer. You publish a custom event with applicationEventPublisher.publishEvent(new OrderPlacedEvent(...)) and any @EventListener method in the application context receives it. Spring wires the subscription automatically via its application-context configuration.
java.util.Observable (deprecated since Java 9) was the JDK's built-in observer. Its concrete-class design forced inheritance instead of composition, which made it less flexible than an interface-based approach. The lesson is to prefer an observer interface when you control the contract.
Reactor / RxJava takes Observer to its logical conclusion. A Flux or Observable stream is a subject. Subscribers register via .subscribe(). The pattern is the same, just wrapped in a reactive API with backpressure, error handling, and operator chaining built in.
30-Second Explanation
Observer lets one subject notify many independent listeners through an interface. The subject publishes an event and does not know the concrete reactions, so adding a listener usually changes wiring rather than publisher code. By default this example is in-process and synchronous; durability and retries require additional infrastructure.
5-Minute Explanation
Start with a service that directly calls email, inventory, and analytics after saving an order. Introduce the subject, listener interface, concrete listeners, and subscription lifecycle. Walk through the synchronous flow: save the order, create an immutable event, iterate listeners, and apply the chosen failure policy. Then discuss practical decisions: use a stable event contract, unsubscribe listeners with shorter lifetimes, isolate failures when reactions are independent, and move to a queue when delivery must survive process or network failure.
When to Use / When NOT to Use
Use when:
- One event triggers multiple independent reactions (email, logging, cache invalidation)
- You want to add new reactions without modifying the event source
- Listeners are loosely coupled and do not depend on each other's results
- The publisher and all observers live in the same JVM process
Skip when:
- You need durable delivery or retries across process boundaries (use a message queue instead)
- The "observers" need to respond and the publisher needs their result (that is request/response, not observation)
- There is only one listener and it will never change (a direct method call is simpler)
- Listeners have ordering dependencies on each other (consider a pipeline or chain of responsibility)
If multiple things need to react to one event and they do not need each other's output, you need Observer. If they need coordination, look at Mediator instead.
The "push vs pull" observer variant
There are two flavors of Observer. Push (what we implemented) sends the event data to every listener. Pull sends a minimal notification, and each listener queries the subject for the data it needs:
// Push model: event carries all data. Most common.
void onOrderPlaced(OrderEvent event);
// Pull model: listener queries the subject.
// Less common, but useful when observers need different slices of state.
void onStateChanged(OrderService source);
// Inside the listener:
// Order latest = source.getLastOrder();
Push is simpler and more common. Pull is useful when the subject's state is large and different observers need different subsets. Java Swing's PropertyChangeEvent uses push. Android's LiveData uses pull (observers access getValue() after notification).
Common Mistakes and Misconceptions
These mistakes make an Observer design unreliable or difficult to evolve. Use them as review checks.
-
Forgetting thread safety. Using
ArrayListfor the observer list while subscribe/notify can overlap creates a race. Choose a thread-safe collection such asCopyOnWriteArrayList, or use explicit synchronization. -
Not deciding how listener failures behave. If observer A throws, observers B and C may not fire in a synchronous loop. If listeners are independent, isolate each call; if the event is transactional, define how failure is surfaced or retried.
-
Confusing Observer with Pub/Sub. Observer is commonly in-process and synchronous by default. Pub/Sub systems such as Kafka or RabbitMQ add network delivery, persistence, and retry semantics. They solve related but different problems.
-
Making the observer interface too fat. An
OrderObserverwithonPlaced(),onShipped(),onCancelled(),onRefunded()forces every listener to implement methods it does not care about. Use one interface per event type, or use the genericEventListener<T>approach. -
Leaking references (memory leak). If observers subscribe but never unsubscribe, the subject holds references forever. In long-lived systems, this causes memory leaks. Mention
WeakReference-based lists or explicit lifecycle management. -
Ordering assumptions. Treating observers as if they fire in a required order creates hidden dependencies ("analytics must fire after email"). Observers are a better fit when reactions are independent; if ordering matters, use a chain of responsibility or an explicit pipeline.
Observer vs Mediator vs Event Bus
When you need to decouple components that react to events, three patterns compete. The decision hinges on topology and who owns the routing logic.
| Dimension | Observer | Mediator | Event Bus |
|---|---|---|---|
| Topology | 1-to-many (one publisher) | Many-to-many through one hub | Many-to-many, loose |
| Coordination | None, pure broadcast | Active, mediator has logic | Routing rules only |
| Coupling | Publisher knows the interface | Components know the mediator | Components know the bus API |
| Delivery | Synchronous by default | Synchronous, in-process | Often async, in-process |
| Best for | Reactions to a single event source | Complex component interactions | Decoupled module communication |
Do not choose an event bus when a single subject and a small listener list are enough. Observer is simpler in that case; an event bus adds a shared object that many modules can publish to and subscribe from. Use the simplest pattern that solves the communication need.
Test Your Understanding
Each scenario presents a code problem. Identify the issue, then expand "Show Answer" to check your reasoning.
Quick Recap
These are the key points to internalize when applying Observer.
- Observer decouples a subject from its reactions. The publisher fires events to a list of listeners without importing or knowing any concrete listener class.
- Use a thread-safe observer collection when subscriptions and notifications can overlap.
CopyOnWriteArrayListis useful when writes are rare because iteration sees a stable snapshot. - Decide how listener failures should behave. For independent listeners, isolate each invocation so one failure does not prevent the remaining listeners from executing.
- Use async dispatch (
ExecutorService) only when a slow listener measurably blocks the publisher. Start synchronous, measure, then optimize. - Pass an immutable event object (Java record) instead of raw arguments. A stable event type lets the publisher evolve its data without changing every listener signature.
- Basic Observer is in-process. For cross-service event delivery with durability, use a message queue (Kafka, SQS, RabbitMQ) instead.
- Distinguish Observer from Pub/Sub by scope and delivery semantics: Observer is usually in-process, while a broker can provide cross-service durability and retries.
- The push model (event carries data) is simpler and more common. Use pull (observer queries subject) only when different observers need different subsets of a large state.
- Never assume observer execution order. If ordering matters between observers, you have a hidden dependency. Use separate phase lists or a pipeline instead.
Related Patterns
- Mediator pattern - Coordinates interactions and makes routing decisions; Observer broadcasts notifications to independent listeners.
- Command pattern - Encapsulates an action; an observer can receive an event and choose to create or dispatch a command.
- Strategy pattern - Selects one algorithm; Observer supports notifying multiple reactions to a change.
Related Articles
The strategy pattern extracts a family of algorithms behind an interface so the client can swap behaviors at runtime without touching the context class.
The mediator pattern routes all communication between objects through a central coordinator, replacing an O(N-squared) mesh of direct references with N connections to one hub.
The command pattern turns requests into objects so you can queue, log, and undo operations. Decouple sender from receiver by encapsulating actions.