When would you use the observer pattern, and what goes wrong with it at scale?
Observer decouples a subject from things that react to it, which is right when the subject genuinely should not know its listeners. The costs are a control flow you cannot read from the code, listener lifetimes that leak, and an exception in one observer that silently prevents the rest from running.
What the interviewer is scoring
- Does the candidate name the loss of readable control flow as the main cost
- Whether the listener lifetime and leak problem is raised without prompting
- That failure handling across multiple observers is treated as a design decision
- Whether they consider whether notification happens on the subject's thread
- Does the candidate distinguish in-process observer from an event bus or message broker
Answer
What it is for
Observer applies when one object changes and an open-ended set of others need to react, and the changing object has no business knowing who they are. A shopping cart whose total changes should not import the analytics client, the badge renderer and the shipping estimator. It publishes that the total changed, and whoever cares subscribes.
The test for whether it fits is that last clause. If the subject genuinely should not know its listeners — because they are in higher layers, because they vary by deployment, because they will be added by code the subject's author will never see — the indirection is buying something. If there are exactly two listeners, both known at compile time and both in the same module, a direct call is clearer and you should make it.
The cost is that you cannot read the program
Every criticism of observer is a variation on one thing: the control flow no longer exists in the source.
Trace what happens when a cart total changes and you find notifyObservers(). Where execution goes next depends on what subscribed at run time, which depends on wiring that may be in a different module, may be conditional on configuration, and may have happened in an order that varies. The call graph tools cannot follow it, and neither can a person.
This is tolerable at five listeners and awful at fifty, and the degradation is worse than linear because of chaining. An observer that itself publishes an event, which another observer handles by publishing a third, produces a cascade nobody designed and nobody can see. The pathological case is a cycle — A notifies B, B updates something that notifies A — which either recurses until the stack overflows or, if there is a guard, quietly drops an update. Both failures are extremely hard to diagnose from the symptom, because the stack trace shows the mechanism rather than the intent.
Listeners leak
The classic memory leak in any language with a garbage collector. The subject holds a reference to every observer, so an observer stays reachable for as long as the subject lives, whether or not anything else refers to it.
Register a listener from a component with a short life — a UI view, a request-scoped service — and forget to unregister, and that component and everything it references stays alive until the subject dies. Since subjects are typically long-lived, that is often the life of the process. The symptom is a heap that grows steadily under normal use with no obvious culprit, and it is usually found only in a heap dump.
Unregistering is the fix, and it is unreliable in exactly the way manual resource management always is: it must happen on every path including the exceptional ones. Weak references shift the problem rather than solving it — the listener now becomes collectable, which means it can be collected while you still wanted it, and the failure mode changes from a leak to notifications that silently stop arriving. Weak listeners are appropriate where a natural owner keeps the observer alive for as long as it should live; used as a general safety net they trade a visible bug for an invisible one.
Two decisions the pattern does not make for you
What happens when an observer throws. The naive loop calls each observer in turn, so the first exception aborts the loop and every observer after it is never called. Which observers those are depends on registration order, so the failure is both partial and arbitrary. You have to decide deliberately: isolate each call and collect the failures, let the exception propagate and fail the whole notification, or something in between. Any of the three can be right; not choosing is what is wrong.
Which thread runs the observers. By default the subject's, which means an observer doing something slow — an HTTP call, a disk write — blocks the code that published the event, and a subject inside a lock now runs arbitrary third-party code while holding it, which is a deadlock waiting for the right listener. Handing notification to an executor fixes that and introduces ordering questions and the possibility of observers running after the state they were told about has changed again. Neither answer is free, and the choice belongs to the subject's author.
The line between this and messaging
Observer is in-process, synchronous by default, and loses everything on restart. An event bus adds indirection and often asynchrony but is still one process. A message broker adds durability, cross-process delivery, retries and at-least-once semantics.
The temptation as a system grows is to slide along that line by accident — an observer, then an event bus, then a bus that persists events, arriving at a badly implemented message broker. Crossing that boundary should be a decision. The signal that it is time is durability: the moment it matters that a notification survives a crash, in-process observation is the wrong tool and no amount of hardening will make it the right one.
The pattern buys you a subject that does not know its listeners. The bill is a control flow that exists only at run time, and you pay it every time someone has to debug the resulting cascade.
Likely follow-ups
- One observer throws. What should happen to the other five, and who decides?
- Why do observers leak, and what does a weak reference actually fix?
- Should a listener be notified on the publisher's thread? What changes if not?
- At what point would you replace an in-process observer with a message broker?
Related questions
- Give me a real single-responsibility violation you have seen, and how would you refactor it?mediumAlso on coupling5 min
- When would you use the strategy pattern instead of inheritance?mediumAlso on behavioural-patterns5 min
- Service A needs something from service B. When should that be a synchronous call and when should it be an event?mediumAlso on coupling3 min
- Adapter, decorator and facade - what concrete problem does each one solve, and where would reaching for it be over-engineering?mediumAlso on coupling6 min
- How do goroutines leak, and how would you find a leak in a running service?mediumAlso on memory-leaks5 min
- What is a closure, and how does one end up leaking memory?mediumAlso on memory-leaks4 min
- How does your order placement change on a venue that allocates pro rata within a price level instead of first in, first out?hardSame kind of round: concept6 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: concept7 min