- Low Level Design
- /
- Observer Pattern
Course content
The Observer pattern decouples a thing that changes from the things that need to react to the change. The thing that changes is called the Subject. The things that react are Observers. Observers register with the Subject; when the Subject's state changes, it walks its list of registered observers and calls a method on each one. The Subject does not know what observers do with the notification, and it does not need a separate code path for each kind of observer.
The payoff is one-to-many fan-out without coupling. A new observer is a new class that implements the observer interface and calls subject.subscribe(this). The Subject is never edited, none of the existing observers are touched, and the new observer's bug or slowness cannot break the others. This is the same shape as the publish/subscribe model on a message broker, just running entirely inside one process.
Observer is the right tool whenever the answer to "who needs to know about this?" is open-ended or likely to grow. If an order being shipped should trigger an email today, an SMS next month, an audit-log entry next quarter, and an analytics event after that, the order code should not have to grow a new line of orchestration each time. It should just announce that the order shipped, and let interested parties subscribe. The page starts from the version where the producer knows everyone by name.
OrderService.markShipped knows about every consumer of the shipping event by name. When marketing asks for a push notification, the constructor grows another parameter and the method grows another line. When the warehouse wants to trigger restock checks, same thing. The class that is supposed to manage orders has become an orchestration script for everything that happens after an order ships.
The constructor is a separate problem. With five collaborators today and ten next year, every test of OrderService has to construct ten stubs, even when the test cares about exactly one of them. The blast radius of a bug in EmailService is now "every test of OrderService is uglier".
The behavioural risk is the worst piece. If email.sendShippedEmail throws, the whole markShipped call throws, and the next three notifications never happen. Worse, if the order has already been written to the database, the system is now in an inconsistent state: shipped in the DB, no audit log, no analytics. The coupling is not just structural; it is operational.
What this costs in production
Marketing asked for one more notifier before a holiday sale, so OrderService grew another constructor argument and another inline call. Under load the new SMS provider started timing out, and because the call ran inline before the audit and analytics steps, every order after it was written to the database as shipped but never logged or tracked. A one-line feature quietly corrupted the shipping records until the on-call engineer disabled it.
The order service is one instance of a producer hard-wired to its consumers. In review, the symptoms look like these.
| Smell | What it looks like | Why Observer is the answer |
|---|---|---|
| A method ends with a hardcoded sequence of side-effect calls to known collaborators | After updating the order, call emailer, then sms, then auditLog, then analytics, in order | The list grows every time a new consumer is added, and ordering becomes a coupling the caller did not sign up for |
| Adding a new consumer means editing the producer | A request to also notify the warehouse means a new line in OrderService.markShipped | The producer becomes a magnet for change for reasons that are not its responsibility |
| The producer takes a constructor parameter for every possible consumer | OrderService(EmailService, SmsService, AuditLog, Analytics, Warehouse, ...) | The constructor balloons; testing a single behaviour requires stubs for all collaborators |
| A failing or slow consumer can break the producer | If the email service times out, the order does not get marked shipped | The consumers should be advisory; one of them being unhealthy should not block the producer |
| Different deployments need different sets of consumers | Production wires all four; tests want only the audit log; staging wants email but not SMS | A subscribe-list is a natural place to vary the set of consumers per environment |
Each consumer is now a small object with one responsibility: take an Order, do its piece. They are independent of OrderService and of each other, so a slow email service no longer blocks the audit log. The try/catch around the notify loop turns each observer into an advisory that cannot poison the producer.
OrderService itself no longer knows the list of consumers. Adding a push notification next month is a new file: write PushObserver implements OrderObserver, register it at startup, ship. None of the existing code changes, so none of the existing tests have to be re-run for behavioural reasons.
The subscribe/unsubscribe pair is what makes the pattern useful in tests and in environments. In a unit test, you subscribe a tiny RecordingObserver and assert that it received the right events. In production, you subscribe the real notifiers at startup. In staging, you subscribe a different subset. None of these wirings change OrderService because the wiring is data, not code.
Observers come in two flavours that differ in how much information the Subject hands them. The push flavour bundles the change into the notification: observer.onOrderShipped(order, timestamp). The pull flavour just signals that something changed and lets observers ask back for whatever they need: observer.onChange(this) followed by the observer calling subject.getOrder().
Push is simpler when the notification is self-contained and the data the observer needs is small. Most Java event systems are push: MouseEvent carries the click coordinates, PropertyChangeEvent carries the old and new values. Pull is useful when the observer might need different parts of the Subject's state and you do not want to put every field into every notification. Java's java.util.Observable (now deprecated) was a hybrid: the notification carried an optional arg object and observers could query the Observable for more.
For most everyday LLD work, the push flavour is the one you want. It is type-safe, the notification is self-describing, and you do not pay for callbacks that fetch data they will not use.
With the refactor in hand, it is worth pinning down what Observer is not, because several related ideas share the announce-and-react shape.
Observer vs. Strategy vs. Chain of Responsibility: all three hang multiple implementations off one interface, and the difference is head count at the call site. A Strategy context holds exactly one implementation and calls it. An Observer subject holds a list and notifies all of them. A chain passes the request along until one handler claims it, and the handlers after that one never see it. When you sketch the subject in an interview, say the cardinality out loud ("the subject notifies every registered observer"); it is the sentence that stops the three patterns blurring together.
Observer vs. Mediator: in Observer, all the observers know about the same Subject and react independently. The Subject does not coordinate them; it just announces. In Mediator, a central coordinator wires up many components that talk through it, often bidirectionally. Mediator is the right pattern when components need each other's behaviour to compose; Observer is the right pattern when they merely need to know that something happened.
Observer vs. Pub/Sub broker: Observer runs in-process and the Subject holds direct references to its observers. A pub/sub broker (Kafka, RabbitMQ, SNS) is the same shape but extracted to a separate process, with persistence, retries, and decoupled deployment. If the publisher and subscribers are in the same process and you do not need durability or at-least-once semantics, Observer is the right level of tool. The moment you need the notification to survive a publisher crash, reach subscribers in another process, or fan out to thousands of consumers, you have outgrown Observer and you want a broker.
Observer vs. raw callbacks: a single callback (Runnable, Consumer<T>) is Observer with one observer and no subscribe interface. That is fine until you need a second observer; at that point you are inventing a list, a subscribe call, and a notify loop, and you may as well use Observer from the start.
The java.beans.PropertyChangeListener and PropertyChangeSupport pair is classic Observer in the JDK: a bean fires firePropertyChange("name", oldValue, newValue) and every registered listener is called. The addXxxListener/removeXxxListener convention is Observer in everything Swing-related (ActionListener, MouseListener, KeyListener), with the component as the Subject. Spring's ApplicationEventPublisher and @EventListener automate the subscription step: annotate a method and the framework registers it as an observer of that event type, so publishEvent(new OrderShippedEvent(order)) walks every matching listener. And java.util.Observable is the cautionary tale: the JDK's original take was deprecated in Java 9 because raw Object payloads, mutable change flags, and missing thread safety aged badly. Even a great pattern can be implemented poorly.
Django signals are the same machinery with the registry owned by the framework: post_save.connect(handler) subscribes a receiver, and saving a model notifies every connected one, which is exactly subscribe plus the notify loop. The blinker library packages the standalone version for plain Python. In GUI land, PyQt and PySide expose Qt's signal-slot system, where button.clicked.connect(handler) reads almost like prose: the signal is the Subject, every connected slot is an observer.
Qt's signals and slots are also the most famous Observer implementation in C++: connect(button, &QPushButton::clicked, this, &Dialog::onClicked) registers an observer, and emitting the signal notifies every connection. Without a framework, the idiomatic C++ form is a std::vector<std::function<void(const Order&)>> on the subject, a subscribe method that pushes into it, and a notify loop that calls each one; Boost.Signals2 packages that shape with thread safety and automatic disconnection built in.
| Pros | Cons |
|---|---|
| Producer is decoupled from consumers; new consumers are additive | Notification order is implicit and can become a hidden contract |
| Each observer is independently testable and swappable | Memory leaks if observers forget to unsubscribe (a common cause of GUI leaks) |
| A failing observer does not have to break the producer | Synchronous notification ties producer latency to slowest observer (unless you go async) |
| Same shape works for unit-test recorders and production fan-out | Debugging "why did this method run?" gets harder; the trigger is registered far away |
| Subscribe/unsubscribe lets you vary the observer set per environment | Cyclic notifications (observer mutates the Subject) can cause re-entrancy bugs |
Observer pays off when there are multiple, possibly varying consumers of an event, or when there will be. If there is exactly one consumer and there is no plausible reason for a second, a direct call is fine. Wrapping a single dependency in subscribe/notify adds two indirection points and a new vocabulary for no payoff.
A second failure mode is leaking strong references. The Subject holds the list of observers; until they unsubscribe, they stay alive. In long-running processes (servers, GUIs), an observer that never unsubscribes is a memory leak. The standard fix is WeakReference-based observer lists, or more usually, a clear lifecycle convention ("register on activate, unregister on deactivate").
The third failure mode is order-of-notification dependencies. Once two observers have to run in a specific order, the pattern is leaking; the design wants a Mediator or a domain service that owns the ordering. Treat "these two observers must run in this order" as a smell, not a constraint to encode.
Three flavours of question recur. The first is design: "design a stock-price ticker / chat-room / notification system". The expected first move is to identify the Subject (the price feed, the room, the event source) and let interviewers see you sketch the observer interface and the subscribe/unsubscribe pair. The follow-up tends to be about scale, which is where you contrast in-process Observer with a real broker.
The second is comparison: "Observer vs. publish/subscribe vs. mediator". The interviewer wants to see that you know Observer is the in-process specialisation of pub/sub, and that mediator is a different thing (centralised coordination, not announcement). The trap is to call everything "pub/sub"; the difference is real and interviewers do hear it. The cardinality contrast with Strategy and Chain of Responsibility is worth one rehearsed sentence too: one held, all notified, first-to-claim.
The third is about failure handling and threading: "what if an observer is slow?" or "what if an observer throws?". The expected moves are to wrap each notify call in a try/catch so one bad observer does not poison the others; to consider running notification asynchronously (executor pool) if any observer is slow; and to avoid holding a lock across the notify loop because observers might re-enter the Subject.
The companion exercise gives you an OrderService that hardcodes calls to four known notifiers and asks you to refactor it into an Observer Subject with subscribe, unsubscribe, and a notify loop. The validator checks each observer in isolation, that subscribers are notified when an order ships, that unsubscribed observers are not notified, that an entirely new observer defined inside the validator works without touching OrderService, and that one observer's exception does not block the others.
Open the exercise: Observer: Refactor OrderService Notifications.