Home › Low-Level Design › Observer Pattern
O

Observer Pattern

One object changes, and a bunch of other objects need to know about it. Observer is how you wire that up without hard-coding who listens to whom.

Low-Level DesignMedium10 min read

This one comes up on the whiteboard constantly. It looks simple, and then people get the details wrong under pressure, and at a top-company loop the details are the whole grade. Here is how to actually think about it, including the two or three sentences that separate a "hire" from a "lean hire."

If you remember five things

The whole pattern in one glance. Everything below defends these five calls.

  1. Invert the dependency. The subject depends on an Observer interface, never on concrete listeners. That single indirection is the entire pattern.
  2. Subject owns state, observers react. The subject holds the list and notifies on change. It never knows who is in the list or what they do with the news.
  3. Push or pull, on purpose. Push hands data into update(); pull signals "something changed" and lets observers read back. Default to push, reach for pull on large state.
  4. The lapsed listener leaks. A subject that keeps a reference to a discarded observer blocks garbage collection. Unregister in teardown, or hold observers weakly.
  5. The pattern says nothing about order, re-entrancy, or slow observers. If you need those guarantees, you are fighting the pattern and want a pipeline or mediator instead.

1The problem it solves#

Start with the pain, because the pattern only makes sense against the thing it kills.

  • Say you are building a stock ticker. A Stock object holds the current price.
  • When the price moves, three things have to react: a price chart redraws, an alert service checks if someone wanted a ping at $150, and a logger writes the change to disk.

The naive version looks like this:

class Stock {
    void setPrice(double price) {
        this.price = price;
        chart.redraw(price);
        alertService.check(this.symbol, price);
        logger.log(this.symbol, price);
    }
}

This works right up until it does not.

  • Marketing wants an email when a stock crosses a threshold. So you open Stock and add a line.
  • Then someone wants a mobile push. Open Stock again.
  • The Stock class now imports chart libraries, email clients, and push SDKs. It knows about everyone.
  • Every new reaction means editing the one class that should only care about price.

The smell, named precisely.

  • A subject that owns a piece of state is also responsible for knowing every party interested in that state. Those are two different jobs.
  • Weld them together and you touch core code for every peripheral feature.
  • It breaks the Single Responsibility Principle: Stock now has two reasons to change, the pricing rules and the list of reactors.
  • It breaks the Open-Closed Principle: adding a reaction forces a modification instead of an extension.
The one idea an interviewer is grading

This whole pattern is about which way the dependency arrow points. If the subject names its listeners, you have failed the question no matter how clean the code looks. If the subject names only an interface, you have passed it before you write the second class.

2The idea (mental model)#

Flip the relationship so the subject stops reaching out and starts being subscribed to.

  • Instead of the Stock calling each interested party by name, interested parties register themselves with the stock.
  • The stock keeps a list of "things that asked to be told."
  • When its state changes, it loops over that list and says "something changed."
  • It does not know or care who is in the list or what they do with the news.

The group-chat analogy.

  • The sender posts once. Everyone subscribed gets the message.
  • People join and leave the chat freely.
  • The sender never maintains a personal contact list for each announcement.

Two roles, and that is the whole cast.

  • Subject (also Observable or Publisher): owns the state, holds the list of observers, notifies them on change.
  • Observer (Subscriber or Listener): implements a known method the subject can call, like update(...).

The trick is a single indirection.

  • The subject depends on an interface, not on concrete observers.
  • The subject says "I will call update() on whatever is in my list," and the list can hold a chart, a logger, an email sender, anything that honors the interface.
  • This is the Dependency Inversion Principle in one move: the high-level Stock and the low-level EmailNotifier both depend on the Observer abstraction, not on each other.

3Structure#

Four moving pieces, and the arrows between them are the real content.

  • A Subject interface with register, unregister, and notify.
  • A concrete subject that holds state and the observer list.
  • An Observer interface with an update method.
  • Concrete observers that do the actual work.

Dependency direction, which is worth pointing at on the board.

  • The concrete subject points at the Observer interface.
  • The concrete observers point at the Observer interface too (they implement it).
  • Nobody points at a concrete observer class.
  • That is what keeps the subject open to new listeners without edits. Draw that arrow correctly and you have basically answered the question.
classDiagram class Subject { +registerObserver(o) void +removeObserver(o) void +notifyObservers() void } class Stock { -String symbol -double price -List observers +setPrice(price) void +registerObserver(o) void +removeObserver(o) void +notifyObservers() void } class Observer { +update(symbol, price) void } class PriceChart { +update(symbol, price) void } class AlertService { +update(symbol, price) void } Subject <|.. Stock Observer <|.. PriceChart Observer <|.. AlertService Stock o-- Observer
The four pieces and their arrows. Note that every arrow terminates on an interface (Subject or Observer), never on a concrete class. That is the shape that lets you add a new observer type without reopening Stock.

One detail a strong candidate calls out: the aggregation.

  • Note the aggregation (o--) between Stock and Observer. The observers live independently of the stock.
  • The stock just holds references to them. It does not own their lifecycle and does not create or destroy them.
  • That is the correct relationship here (aggregation, not composition): a chart or an alert service outlives any single stock it happens to watch.

4Implementation in Java#

Clean, no framework, shows the design. This is the version to write at the board.

// The contract every listener honors.
interface StockObserver {
    void update(String symbol, double price);
}

// The thing observers can register with.
interface Subject {
    void registerObserver(StockObserver o);
    void removeObserver(StockObserver o);
    void notifyObservers();
}

class Stock implements Subject {
    private final String symbol;
    private double price;
    private final List<StockObserver> observers = new CopyOnWriteArrayList<>();

    public Stock(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public void registerObserver(StockObserver o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(StockObserver o) {
        observers.remove(o);
    }

    @Override
    public void notifyObservers() {
        for (StockObserver o : observers) {
            o.update(symbol, price);
        }
    }

    public void setPrice(double price) {
        this.price = price;
        notifyObservers();   // state changed, tell everyone
    }
}

Now the observers. Each one does its own thing and knows nothing about the others.

class PriceChart implements StockObserver {
    @Override
    public void update(String symbol, double price) {
        System.out.println("Chart: redraw " + symbol + " at " + price);
    }
}

class AlertService implements StockObserver {
    private final double threshold;

    public AlertService(double threshold) {
        this.threshold = threshold;
    }

    @Override
    public void update(String symbol, double price) {
        if (price >= threshold) {
            System.out.println("Alert: " + symbol + " hit " + price);
        }
    }
}

And wiring it together:

Stock apple = new Stock("AAPL");
apple.registerObserver(new PriceChart());
apple.registerObserver(new AlertService(150.0));

apple.setPrice(148.0);   // chart redraws, no alert
apple.setPrice(152.0);   // chart redraws, alert fires

The payoff, stated plainly.

  • Adding an email notifier later means writing one new class that implements StockObserver and calling registerObserver.
  • You never reopen Stock.
  • That is the Open-Closed Principle paying rent.

Two deliberate choices worth saying out loud.

  • The CopyOnWriteArrayList is not an accident. If an observer removes itself (or another observer) during a notification, a plain ArrayList throws ConcurrentModificationException. Copy-on-write sidesteps that because iteration runs over a snapshot. It also fits observer lists exactly: you read (notify) often and write (register, remove) rarely, which is the workload it is tuned for.
  • The data goes into update(symbol, price), which is the push model. The alternative is the pull model, where update() just signals "something changed" and the observer calls back into the subject to read what it needs.
Default
Push model
What update() carries
The changed data itself, like (symbol, price).
Observer work
Use what you were handed, no callback.
Best when
The payload is small and most observers want the same thing.
Breaks when
State is large and each observer wants a different slice, so you push fields nobody reads.
Reach for on large state
Pull model
What update() carries
Just the subject, like update(Stock s).
Observer work
Call back into the subject and read only the fields it cares about.
Best when
State is large and observers each want different slices.
Breaks when
Adds a round trip and lets observers reach into subject internals, which couples them.
From the interviewer's chair

I do not care which model you pick. I care that you know there are two and can say why you chose one. "Push, because the payload is one price and every observer wants it" is a complete answer. Picking push silently, as if it were the only option, tells me you learned the shape but not the trade-off.

5A real-world example#

You have used this pattern whether you named it or not.

  • UI event listeners. Every button click handler is Observer. The button is the subject, your onClick is the observer. The button does not know what your handler does, it just calls it.
  • Model-View architectures. The model is the subject, views are observers. Change the data once and every view showing it refreshes. This is the backbone of a lot of frontend state management.
  • Java's own libraries. PropertyChangeListener in Swing and the listener interfaces all over the JDK. Reactive libraries (the Flowable and Observable types in RxJava) are Observer with backpressure and operators bolted on.

One versioning note a careful candidate mentions.

  • The old java.util.Observer and java.util.Observable were deprecated in Java 9.
  • If you reference the JDK, reference the newer listener interfaces or reactive streams, not the deprecated pair.

Pub/sub and message queues are Observer scaled across machines, with one real difference.

  • Kafka, Redis pub/sub, and the like keep the same shape in a bigger box: publishers do not know their subscribers, and the broker holds the subscription list.
  • The difference to name: an in-process Observer call is synchronous and in-memory.
  • A message broker adds durability, async delivery, and retries, so it survives a subscriber being offline. An in-memory observer list does not.
When the prompt is secretly an Observer question

In LLD rounds, Observer hides inside bigger designs: a notification service, a logging system, an auction where bidders watch an item, a chat room broadcasting to members. Any time the prompt says "when X happens, these other things should react," your first instinct should be Observer.

6When to use it, and when not to#

Reach for it when:

  • One change needs to fan out to many independent reactions.
  • The set of reactions is not fixed. Things subscribe and unsubscribe at runtime.
  • You want the source of the change decoupled from the things reacting to it.

Skip it when:

  • There is exactly one reaction and it will always be one. A direct method call is clearer. Do not add a listener interface to feel clever.
  • The relationships form a web where A notifies B notifies C notifies A. Observer makes cascading updates easy, which is the rope you hang yourself with. Cycles are brutal to debug because the stack trace is a loop and nothing tells you which hop re-triggered the first.
  • Order of notification matters and you need guarantees. The pattern says nothing about order. If observer B must run after observer A, you are fighting the pattern and probably want an explicit pipeline or a mediator.

Observer versus a full event bus, since interviewers probe the line.

  • Plain Observer is a direct, in-process, synchronous call from one subject to its own list of listeners.
  • An event bus adds a middleman: publishers and subscribers never see each other, events are typed and routed, and delivery can be async or buffered.
  • Use plain Observer when one subject has a known handful of listeners. Use a bus when many unrelated producers and consumers need to meet without any of them holding a reference to the others.

7Common mistakes#

The one that actually leaks memory in production

The lapsed listener. An observer registers, then the rest of the app throws it away, but the subject still holds a reference to it in its list. The garbage collector cannot reclaim it. Do this a few thousand times in a long-lived UI or server process and you have a memory leak that profiles as a slowly growing observer list. The fix: always unregister in a teardown path, or hold observers through WeakReference if the subject genuinely should not keep them alive. This is a real failure mode, not a theoretical one, and naming it unprompted is a strong signal.

Modifying the list mid-notify (re-entrancy).

  • If update() triggers a register or remove on the same subject while you are looping, a plain list blows up with ConcurrentModificationException.
  • Fix: copy-on-write, or iterate over a defensive snapshot.

Order dependence sneaking in.

  • People write observers that quietly assume another observer already ran.
  • Works today because of insertion order, breaks the day someone registers in a different sequence.
  • If you feel this creeping in, the pattern is the wrong tool.

Doing heavy work synchronously in update().

  • If one observer makes a slow network call inside update(), it blocks the subject and every observer after it in the loop.
  • One observer that takes 2 seconds stalls the entire notify.
  • In a real system, hand slow work to a queue or an executor so update() just enqueues and returns. Say this out loud, it shows you think past the toy example.

Swallowing or propagating exceptions wrong.

  • If one observer throws inside the loop, do the rest still get notified? Decide on purpose.
  • Usually you catch per observer, log it, and keep going, so one bad listener does not blind the others.
  • An interviewer who asks "what if observer 3 throws?" is checking whether you thought about this.

Leaking too much state.

  • In push mode, do not dump the subject's entire internal state into update(). Pass what observers actually need.
  • Over-sharing couples observers to the subject's internals and defeats half the point.
Pause before you read on: your `Stock` uses a plain `ArrayList`, and one observer's `update()` calls `removeObserver(this)` to unsubscribe after it fires once. What happens, and what are two ways to fix it?
Reveal the answer
The loop in `notifyObservers()` is iterating the list while `update()` mutates it, so you get a `ConcurrentModificationException` mid-notify. Two clean fixes: swap to a `CopyOnWriteArrayList` so iteration runs over a snapshot and writes do not disturb it, or iterate over a defensive copy of the list and apply the removal to the real list afterward. A third option is to collect self-removal requests during the loop and flush them once it finishes. The underlying lesson is that notification and subscription changes race, and the data structure has to account for it.

8Where it shows up in interviews#

Interviewers love Observer because it is a clean test of whether you understand dependency direction. Expect prompts shaped like:

  • "Design a notification system." The user is the subject; email, SMS, and push channels are observers.
  • "Design a stock price tracker" or "a weather station with multiple displays." Textbook subject with many views.
  • "Design a chat room, auction, or live-score app." Members or bidders observe a shared subject.

What a grader is listening for when running these.

  1. Do you introduce an Observer interface, or hard-code the concrete listeners into the subject? The interface is the whole answer.
  2. Can you add a new observer type without touching the subject? If yes, you nailed the Open-Closed Principle without being asked.
  3. Do you mention push versus pull, and justify your choice?
  4. Do you catch the lifecycle issue (unregister, leaks) and the concurrency issue (modifying during notify)? Strong signal.
  5. Can you say where it breaks (cycles, ordering, one slow or throwing observer) instead of selling it as a cure-all?

Code the skeleton in two minutes, then talk through those trade-offs, and you have shown exactly what a Medium pattern question is meant to surface. The candidates who pass the loop volunteer the failure modes before anyone asks.

Follow-ups the interviewer will ask (and your answers)
"An observer registers and then the app forgets about it. What goes wrong?"
The lapsed listener leak. The subject still holds a reference in its list, so the garbage collector cannot reclaim the observer. Over a long-lived process the list grows unbounded. Fix it by unregistering in a teardown path, or by holding observers through WeakReference so the subject never keeps them alive on its own.
"When would you use pull instead of push?"
When the subject's state is large and each observer wants a different slice of it. Push would shove the whole payload through every update() call, including fields most observers ignore. Pull hands the observer the subject (update(Stock s)) and lets each one read only the fields it cares about. The cost is an extra callback and tighter coupling to the subject's accessors, so I default to push for small payloads.
"Observer 3 throws inside the notify loop. Do observers 4 and 5 still run?"
Only if I decided so on purpose. By default an uncaught exception aborts the loop and the later observers are skipped. Usually I wrap each update() call in a try/catch, log the failure, and continue, so one bad listener cannot blind the rest. If an observer's failure genuinely must halt the notification, that is a deliberate choice, not an accident of where it sits in the list.
"One observer does a slow network call in update(). What breaks?"
The notify is synchronous and single-threaded, so that one observer blocks the subject and every observer after it in the loop. A 2-second call stalls the whole notification. The fix is to make slow observers enqueue the work to an executor or a queue and return immediately, so update() stays fast. In a distributed version that is exactly why you reach for a message broker instead of in-process calls.
"How is this different from a Kafka or pub/sub system?"
Same shape, different guarantees. Plain Observer is an in-memory, synchronous method call to the subject's own list, so if a listener is down the event is simply lost. A broker adds durability, asynchronous delivery, and retries, and fully decouples publishers from subscribers across machines, so a subscriber can be offline and still receive the event later. Observer is the in-process version; pub/sub is the networked, durable version of the same idea.

What you can now explain in the room

  • Why hard-coding reactions into a subject breaks the Single Responsibility and Open-Closed principles.
  • How depending on an Observer interface inverts the dependency and lets you add listeners without edits.
  • The difference between push and pull, and which to pick for small versus large state.
  • Why the lapsed listener leaks memory and the two ways to stop it.
  • Why modifying the observer list during notify blows up, and how copy-on-write fixes it.
  • Where Observer breaks (cycles, ordering, slow or throwing observers) and when a pipeline, mediator, or event bus fits better.

Next, look at the Publish-Subscribe and Mediator patterns (close cousins when the wiring gets busy), and the "Design a Notification Service" and "Design an Auction System" LLD problems where Observer is the load-bearing piece.

Get a prep plan built around your Observer Pattern interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Observer Pattern prep plan is on the way.