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.
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.
I have seen this exact anti-pattern in every codebase over 50K lines. The "god method" that does one thing, then triggers six side effects inline. The fix is always the same: decouple the side effects from the primary action using Observer.
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 never imports EmailNotifier or InventoryUpdater directly, so adding a new listener requires zero changes to the publisher.
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) never breaks existing listeners.
public record OrderEvent(
String orderId,
String customerEmail,
List<String> itemIds,
double totalAmount,
Instant placedAt
) {}Thread-safe dispatch
CopyOnWriteArrayList is the right default for observer lists. It copies the backing array on every write (subscribe/unsubscribe), so iteration during notification is always safe. The tradeoff is write cost, but observer lists rarely change after startup.
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 the loop never fire. Always wrap each listener call in a try/catch, whether sync or async.
Error isolation strategy
In production, every Observer dispatch loop needs error isolation. Here is the pattern I use on every project:
// Resilient dispatch: log failures, never break the loop.
private void notifyListeners(OrderEvent event) {
for (OrderEventListener listener : listeners) {
try {
listener.onOrderPlaced(event);
} catch (Exception e) {
// Log which listener failed and why. Never rethrow.
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);
}
}
}
This is not optional. In a real system with 5 observers, the probability that at least one throws on any given event is non-trivial. Without isolation, a logging observer crashing takes down your email confirmation flow.
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 is how most modern event systems work. Spring's ApplicationEventPublisher, Guava's EventBus, and Reactor's Flux all use generics under the hood.
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
}
}
I prefer separate lists over 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 classpath scanning. I use Spring events as my go-to example when explaining Observer in interviews because most interviewers know Spring.
java.util.Observable (deprecated since Java 9) was the JDK's built-in observer. It was removed because it used a concrete class instead of an interface, forcing inheritance instead of composition. The lesson: always define observer as an interface.
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.
How to explain Observer in an interview
In an interview, I lead with the "before" code showing three hard dependencies in OrderService. Then I draw the class diagram with the interface between publisher and listeners. The interviewer sees immediately that adding a listener does not touch the publisher. Name the pattern explicitly: "This is the Observer pattern." Then mention the two practical concerns: thread safety (CopyOnWriteArrayList) and exception isolation (try/catch per listener).
If the interviewer asks about scale, explain the jump from in-process Observer to a message queue. The pattern is the same (subscribe, publish, react), but the infrastructure provides durability, retries, and cross-service delivery.
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 guaranteed delivery or durability (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 in Interviews
These are the mistakes I see most often when candidates discuss Observer. Avoid all of them.
-
Forgetting thread safety. Candidates use
ArrayListfor the observer list and forget that concurrent subscribe/notify corrupts the list. Always mentionCopyOnWriteArrayListor explicit synchronization. -
Not catching exceptions per listener. If observer A throws, observers B and C never fire. Wrap each call in try/catch. This is the number one production bug with hand-rolled observers.
-
Confusing Observer with Pub/Sub. Observer is in-process and synchronous by default. Pub/Sub (Kafka, RabbitMQ) is out-of-process with durable queues. They solve different problems. In an interview, name which one you mean and why.
-
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. Some candidates assume observers fire in a specific order and build dependencies between them ("analytics must fire after email"). Observers should be independent. If ordering matters, you need a different pattern (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 |
The mistake I see most often in interviews: candidates say "I will use an event bus" when they mean Observer. Observer is simpler (one subject, N listeners, no routing). An event bus adds a shared bus object that any module can publish to and subscribe from. Use the simplest pattern that solves your problem.
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 you should internalize. Each one is a sentence you could say directly in an interview.
- 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
CopyOnWriteArrayListfor thread-safe observer lists. It copies the array on writes so iteration is always safe, even during concurrent subscribe/unsubscribe. - Wrap each listener invocation in try/catch. One listener failure must never 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. New fields do not break existing listeners.
- Observer is in-process. For cross-service event delivery with durability, use a message queue (Kafka, SQS, RabbitMQ) instead.
- In an interview, name the pattern explicitly, mention
CopyOnWriteArrayListfor thread safety, and distinguish Observer from Pub/Sub by scope (in-process vs cross-service). - 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.