Skip to content

Low Level DesignDesign patterns

Observer pattern

The Observer design pattern through three real examples, from back-in-stock alerts to parking garage events to a live price feed, with diagrams, live demos and code in five languages.

Foundation18 min read

Observer lets one object announce that something happened. Any number of other objects can react, and the announcer doesn’t know who they are. Think of “notify me when it’s back in stock”, a parking garage’s free-space boards, or a trading screen’s live prices. They’re all the same shape: one source of news, and a list of whoever wants to hear it.

The basic version takes ten lines. Real implementations spend most of their code on what the basic version ignores. Listeners that throw. Listeners that are never removed. And listeners that are slower than the news. This page builds up to all three, one level at a time.

The idea in one picture
Product the subject listeners: • Ana (email) • Ben (push) • dashboard "back in stock" EmailAlert → Ana PushAlert → Ben Shop dashboard Cara (SMS) subscribe() each does its own thing with the news
Scroll or pinch to zoom, drag to pan
The subject keeps a list and tells everyone on it. It never knows what they do with the news, and anyone can join or leave.

Every Observer has the same parts:

  • The subject keeps a list of listeners and tells each of them when something happens.
  • The listener interface is the only thing the subject knows about its observers, usually one method.
  • Concrete listeners each react in their own way and know nothing about each other.
  • Usually an event: an immutable value saying what happened.
The general shape

notifies

*

creates

Subject

-listeners: List<Listener>

+subscribe(Listener) : Subscription

+somethingHappens()

«interface»

Listener

+onEvent(Event)

Event

+what

+when

ListenerA

ListenerB

notifies

*

creates

Subject

-listeners: List<Listener>

+subscribe(Listener) : Subscription

+somethingHappens()

«interface»

Listener

+onEvent(Event)

Event

+what

+when

ListenerA

ListenerB

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation

The arrow from the subject points at the interface. The subject can be built, tested and shipped before any listener exists, and it keeps working when one is added or removed.

Easy

Back-in-stock alerts

  • The basic shape
  • Subscribe and unsubscribe
  • Notify on the change that matters

An online shop runs out of trail shoes. Ana asks to be emailed when they’re back. Ben asks for a push notification. The shop’s own dashboard wants to know too. When the restock arrives, all of them should hear about it, and only them. Try it:

Trail shoes

Out of stock

Subscribers (click to subscribe or unsubscribe)

Ana and Ben are subscribed. Restock to see who hears about it.

The first version has the product send the emails itself:

Product.javaproduct.pyProduct.csproduct.goproduct.ts
import java.util.List;
// Before: the product sends the "back in stock" emails itself.
class EmailService {
void send(String to, String text) { System.out.println("email to " + to + ": " + text); }
}
class Product {
final String name;
private int stock;
// flaw 1: the product knows who wants to hear about it, and how to reach them.
private final List<String> waitingEmails;
private final EmailService email = new EmailService();
Product(String name, List<String> waitingEmails) {
this.name = name;
this.waitingEmails = waitingEmails;
}
void restock(int units) {
boolean wasEmpty = stock == 0;
stock += units;
if (wasEmpty && stock > 0) {
// flaw 2: push alerts, SMS, or "update the wishlist badge" each mean editing Product.
for (String to : waitingEmails) email.send(to, name + " is back in stock");
}
}
}
class Main {
public static void main(String[] args) {
Product shoes = new Product("Trail shoes", List.of("ana@example.com"));
shoes.restock(5); // email to ana@example.com: Trail shoes is back in stock
}
}

The product now knows who is waiting and how to reach them. Push notifications, SMS and the dashboard each mean editing Product, a class that should only know about stock. And there’s no way for Ana to stop waiting without someone changing the product’s list.

Flip the dependency. The product keeps a list of anything that implements StockListener, and each way of reaching a person is its own listener:

Easy: a product and the people waiting for it

notifies

*

Product

-stock

-listeners: List<StockListener>

+subscribe(StockListener)

+unsubscribe(StockListener)

+restock(units)

«interface»

StockListener

+backInStock(product)

EmailAlert

+to

PushAlert

+device

notifies

*

Product

-stock

-listeners: List<StockListener>

+subscribe(StockListener)

+unsubscribe(StockListener)

+restock(units)

«interface»

StockListener

+backInStock(product)

EmailAlert

+to

PushAlert

+device

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation
Product.javaproduct.pyProduct.csproduct.goproduct.ts
import java.util.ArrayList;
import java.util.List;
// The listener interface: the only thing a product knows about the people waiting for it.
// Email, push and the wishlist badge all look the same from here.
interface StockListener {
void backInStock(String product);
}
// The subject.
class Product {
final String name;
private int stock;
private final List<StockListener> listeners = new ArrayList<>();
Product(String name) { this.name = name; }
void subscribe(StockListener l) { listeners.add(l); }
void unsubscribe(StockListener l) { listeners.remove(l); }
void restock(int units) {
boolean wasEmpty = stock == 0;
stock += units;
// Notify on the change that matters (empty -> available), not on every restock.
// Otherwise a shop that restocks twice a day spams everyone twice a day.
if (wasEmpty && stock > 0) {
for (StockListener l : listeners) l.backInStock(name);
}
}
void sell(int units) { stock = Math.max(0, stock - units); }
}
// Observers. Each knows how to reach one person one way; none knows about the others.
class EmailAlert implements StockListener {
private final String to;
EmailAlert(String to) { this.to = to; }
public void backInStock(String product) { System.out.println("email to " + to + ": " + product + " is back"); }
}
class PushAlert implements StockListener {
private final String device;
PushAlert(String device) { this.device = device; }
public void backInStock(String product) { System.out.println("push to " + device + ": " + product + " is back"); }
}
class Main {
public static void main(String[] args) {
Product shoes = new Product("Trail shoes");
StockListener ana = new EmailAlert("ana@example.com");
shoes.subscribe(ana);
shoes.subscribe(new PushAlert("ben-phone"));
// A lambda is a listener too: here, the shop's own dashboard.
shoes.subscribe(p -> System.out.println("dashboard: " + p + " available"));
shoes.restock(5); // email to ana, push to ben-phone, dashboard: all three hear it
shoes.restock(5); // nothing: it was already in stock
shoes.unsubscribe(ana); // Ana bought them elsewhere
shoes.sell(10);
shoes.restock(3); // push to ben-phone and dashboard only
}
}
  • The subject knows one interface. Product calls backInStock(name), and nothing else. SMS, a wishlist badge or a webhook is a new listener class. Product never changes.
  • Notify on the change that matters. The product only notifies when stock goes from zero to something. Notifying on every restock would spam everyone twice a day. Deciding what counts as news is the subject’s job.
  • Unsubscribing is part of the design. Ana can leave the list without anyone editing the product. In Go and TypeScript, subscribe returns the function that undoes it, because a function can’t be found in a list again later.
  • A function is a listener. In Python, Go and TypeScript, listeners are just callables. In Java, a lambda fills the one-method interface. Classes earn their place when a listener has settings, like an email address.
Interviewer

We want to add SMS alerts. What changes?

What they are testing

Whether the answer is “a new SmsAlert listener and one subscribe call”, with Product untouched. The red flag is adding an SMS branch inside restock. So is forgetting that people need a way to stop getting alerts.

Say this out loud

The product shouldn’t know who’s waiting for it or how to reach them. I’ll give it a list of StockListeners and notify them when stock goes from zero to available. Email, push and SMS are just different listeners.

Medium

Events in a parking garage

  • Events as values
  • One broken listener
  • Leaks and unsubscribe handles
  • Changing the list mid-notification

When a car parks in a garage, three things should happen. The entrance board shows the new free count. The occupancy metrics go up. And, when the garage is nearly full, an SMS alert goes to the operator. These consumers are owned by different teams, and fail in different ways. The SMS gateway, for one, goes down now and then.

The lot calls each consumer by name:

ParkingLot.javaparking_lot.pyParkingLot.csparking_lot.goparking-lot.ts
// Before: the lot calls everything that cares about a car parking, by name.
class DisplayBoard {
void show(int free) { System.out.println("Entrance board: " + free + " free"); }
}
class OccupancyMetrics {
int parked;
void recordPark() { parked++; }
}
class ParkingLot {
// flaw 1: the lot depends on every consumer. A new one (an SMS alert, a second board)
// means editing ParkingLot, which has nothing to do with boards or metrics.
private final DisplayBoard board = new DisplayBoard();
private final OccupancyMetrics metrics = new OccupancyMetrics();
private int free;
ParkingLot(int spots) { free = spots; }
void park(String spotId) {
free--;
// flaw 2: if the board throws, metrics are never recorded and park() fails,
// even though the car is already in the spot.
board.show(free);
metrics.recordPark();
}
// flaw 3: tests of park() drag in a real board and real metrics.
OccupancyMetrics metrics() { return metrics; }
}
class Main {
public static void main(String[] args) {
ParkingLot lot = new ParkingLot(3);
lot.park("A1"); // Entrance board: 2 free
System.out.println(lot.metrics().parked); // 1
}
}

Three problems. The lot depends on boards and metrics, which have nothing to do with parking. If the board throws, metrics are never recorded and park() fails, even though the car is already in the spot. And a unit test of park() drags in a real board.

The lot publishes a ParkingEvent (what happened, which spot, how many are free now) to a list of ParkingListeners. subscribe returns a handle that unsubscribes.

Medium: the lot publishes events, consumers subscribe

notifies

*

creates

returns

ParkingLot

-listeners: List<ParkingListener>

+subscribe(ParkingListener) : Subscription

+park(spotId)

+leave(spotId)

«interface»

ParkingListener

+onEvent(ParkingEvent)

ParkingEvent

+kind: Kind

+spotId

+freeSpots

«enumeration»

Kind

PARKED

LEFT

«interface»

Subscription

+close()

DisplayBoard

OccupancyMetrics

notifies

*

creates

returns

ParkingLot

-listeners: List<ParkingListener>

+subscribe(ParkingListener) : Subscription

+park(spotId)

+leave(spotId)

«interface»

ParkingListener

+onEvent(ParkingEvent)

ParkingEvent

+kind: Kind

+spotId

+freeSpots

«enumeration»

Kind

PARKED

LEFT

«interface»

Subscription

+close()

DisplayBoard

OccupancyMetrics

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation

Listeners run one after another, inside park(). Break the SMS gateway and switch failure isolation off to see what happens to the listeners after it:

ParkingLot3 free
  1. ·Entrance board
  2. ·SMS "lot nearly full" alert
  3. ·Occupancy metrics

Park a car to notify the listeners, in order.

ParkingLot.javaparking_lot.pyParkingLot.csparking_lot.goparking-lot.ts
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
enum Kind { PARKED, LEFT }
// What happened, as an immutable value. Listeners get the facts, not a reference to the
// lot, so they can't reach back in and change it in the middle of a notification.
// freeSpots is the total, not "+1": a listener that misses an event still shows the truth.
record ParkingEvent(Kind kind, String spotId, int freeSpots) {}
// The only thing the lot knows about its listeners. Boards, metrics and alerts are all
// "something with onEvent", so adding one never touches ParkingLot.
interface ParkingListener {
void onEvent(ParkingEvent event);
}
// Returned by subscribe, so whoever subscribed can stop. A listener that is never removed
// keeps itself and everything it references alive as long as the lot lives: the classic
// Observer memory leak.
interface Subscription extends AutoCloseable {
@Override void close();
}
// The subject.
class ParkingLot {
// Copy-on-write: each notification loops over a snapshot, so a listener that subscribes
// or unsubscribes during a notification can't break the loop. Every add or remove copies
// the array, which is fine here: subscriptions are rare and events are frequent.
private final List<ParkingListener> listeners = new CopyOnWriteArrayList<>();
private int free;
ParkingLot(int spots) { free = spots; }
Subscription subscribe(ParkingListener listener) {
listeners.add(listener);
return () -> listeners.remove(listener);
}
void park(String spotId) {
ParkingEvent event;
synchronized (this) {
free--;
event = new ParkingEvent(Kind.PARKED, spotId, free);
}
// Notify after releasing the lock. A listener that calls back into the lot, or takes
// a lock of its own, can't deadlock with us, and a slow board doesn't hold up other
// cars. The cost: two threads parking at once may notify out of order.
publish(event);
}
void leave(String spotId) {
ParkingEvent event;
synchronized (this) {
free++;
event = new ParkingEvent(Kind.LEFT, spotId, free);
}
publish(event);
}
private void publish(ParkingEvent event) {
for (ParkingListener listener : listeners) {
try {
listener.onEvent(event);
} catch (RuntimeException e) {
// One broken listener must not stop the others, or fail a park() that has
// already happened. Log it and move on.
System.out.println("listener failed: " + e.getMessage());
}
}
}
}
// Observers. Each knows about events; none knows about the others.
class DisplayBoard implements ParkingListener {
private final String name;
DisplayBoard(String name) { this.name = name; }
public void onEvent(ParkingEvent e) { System.out.println(name + " board: " + e.freeSpots() + " free"); }
}
class OccupancyMetrics implements ParkingListener {
int parked;
public void onEvent(ParkingEvent e) { if (e.kind() == Kind.PARKED) parked++; }
}
class Main {
public static void main(String[] args) {
ParkingLot lot = new ParkingLot(3);
Subscription board = lot.subscribe(new DisplayBoard("Entrance"));
OccupancyMetrics metrics = new OccupancyMetrics();
lot.subscribe(metrics);
// A lambda is a listener too; this one stands in for an SMS gateway that is down.
lot.subscribe(e -> { throw new IllegalStateException("SMS gateway down"); });
lot.park("A1"); // Entrance board: 2 free, then listener failed: SMS gateway down
board.close(); // the board is being replaced; stop sending it events
lot.park("A2"); // no board line now, only the SMS failure; metrics still count it
System.out.println(metrics.parked); // 2
}
}
  • One broken listener must not break the subject. The lot wraps each call, logs the failure, and moves on. The car has parked. A listener’s failure is the listener’s problem. C#’s built-in event and most hand-written loops don’t do this. There, one throwing handler stops the rest, and the error shows up in the code that raised the event.
  • Unsubscribe handles stop leaks. The subject holds a reference to every listener. A board that is replaced but never unsubscribed stays in memory, along with everything it references, and keeps receiving events. This is the “lapsed listener” problem, the most common memory leak in UI code. Returning a handle (Subscription, IDisposable, or a function) gives the subscriber exactly what it needs to leave.
  • Loop over a snapshot. A listener may unsubscribe itself, or subscribe another, while it’s being notified. Looping over the live list then throws in Java, and skips an item in Python. Any of these fix it: CopyOnWriteArrayList, an immutable list, or a copy taken under the lock. Listener lists are short and change rarely, so copying is cheap.
  • Notify outside the lock. State changes and the snapshot happen under the lock. The listeners are called after it’s released. Then a listener that calls back into the lot, or takes a lock of its own, can never deadlock with it. There is a cost. Two cars parking at once, on two threads, may notify in either order. That’s why the event carries the total (freeSpots), not “one fewer”. A later event always corrects an earlier one.
  • Events are immutable values. Listeners get the facts, not a reference to the lot. So they can’t change it in the middle of a notification. And the same event can later be sent to another thread or process unchanged.
Interviewer

A listener throws. What happens to the others, and to park()?

What they are testing

Whether you isolate each call, so the event still reaches everyone and park() succeeds. And whether you then ask: if this listener’s work is essential, should it be a listener at all? The red flag is letting the exception escape park(), for a car that has already parked.

Hard

A live price feed

  • Slow subscribers
  • A mailbox per subscriber
  • Choosing what to lose
  • Detecting gaps

A market data feed publishes a price update (a “tick”) every few milliseconds. Its subscribers range from an audit log that keeps up easily, to a ticker, to a chart that redraws slowly on an old laptop. With plain Observer, each tick calls every listener in turn. So the feed can publish only as fast as the chart can draw, and every other subscriber waits for it.

The fix is to stop calling listeners on the feed’s thread at all. Each subscription gets its own mailbox (a bounded queue) and its own worker that empties it. The feed only drops ticks into mailboxes, which takes microseconds however slow the subscriber is.

That raises the real design question: what happens when a mailbox is full? Start the feed and try each answer:

When a mailbox is full:
PriceFeedACME 100.00published 0paused
Audit log100 ms per tick
showing – · behind 0 · dropped 0
Ticker200 ms per tick
showing – · behind 0 · dropped 0
Chart1.5 s per tick
showing – · behind 0 · dropped 0

Hard: the feed fills mailboxes, workers empty them

1

*

calls on its own thread

numbers and creates

PriceFeed

-subscriptions: List<Subscription>

-seq

+subscribe(TickListener, capacity) : Subscription

+publish(symbol, price)

Subscription

-mailbox: bounded queue

-dropped

-worker thread

+offer(Tick)

+close()

«interface»

TickListener

+onTick(Tick)

Tick

+seq

+symbol

+price

Chart

-lastSeq

1

*

calls on its own thread

numbers and creates

PriceFeed

-subscriptions: List<Subscription>

-seq

+subscribe(TickListener, capacity) : Subscription

+publish(symbol, price)

Subscription

-mailbox: bounded queue

-dropped

-worker thread

+offer(Tick)

+close()

«interface»

TickListener

+onTick(Tick)

Tick

+seq

+symbol

+price

Chart

-lastSeq

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation
One tick, two subscribers, two speeds
Chart workerChart mailbox (full)Audit mailboxPriceFeedChart workerChart mailbox (full)Audit mailboxPriceFeedroom: enqueuefull: drop the oldest (publish returns, microseconds laterseesseq = 42offer(offer(take next (chart.onTick(Chart workerChart mailbox (full)Audit mailboxPriceFeedChart workerChart mailbox (full)Audit mailboxPriceFeedroom: enqueuefull: drop the oldest (publish returns, microseconds laterseesseq = 42offer(offer(take next (chart.onTick(
Scroll or pinch to zoom, drag to pan
  • Color key
  • Implementation
  • Entity
Subscription.javasubscription.pySubscription.cssubscription.gosubscription.ts
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
// One price update. seq is assigned by the feed, so a subscriber can tell when it missed some.
record Tick(long seq, String symbol, double price) {}
interface TickListener {
void onTick(Tick tick);
}
// One subscriber's private mailbox and the thread that empties it. This is the whole point
// of the Hard version: the feed only ever touches the mailbox, never the listener, so a slow
// or stuck listener can't slow the feed or anyone else.
final class Subscription implements AutoCloseable {
private final TickListener listener;
private final int capacity;
private final PriceFeed feed;
private final Deque<Tick> queue = new ArrayDeque<>();
private long dropped;
private boolean closed;
private Thread worker;
Subscription(TickListener listener, int capacity, PriceFeed feed) {
this.listener = listener;
this.capacity = capacity;
this.feed = feed;
}
// Runs on the feed's thread. It never blocks and never calls listener code.
synchronized void offer(Tick t) {
if (closed) return;
if (queue.size() == capacity) {
// Full: drop the oldest. For prices only the latest matters, so a stale tick is
// the cheapest thing to lose. (An order feed could never do this; see the page.)
queue.pollFirst();
dropped++;
}
queue.addLast(t);
notifyAll();
}
void start() {
worker = new Thread(this::drain);
worker.start();
}
private void drain() {
while (true) {
Tick next;
synchronized (this) {
while (queue.isEmpty() && !closed) {
try { wait(); } catch (InterruptedException e) { return; }
}
if (queue.isEmpty()) return; // closed and fully drained
next = queue.pollFirst();
}
// Call the listener outside the lock, so the feed can keep offering meanwhile.
try {
listener.onTick(next);
} catch (RuntimeException e) {
System.out.println("listener failed: " + e.getMessage()); // one bad tick, not a dead subscriber
}
}
}
synchronized long dropped() { return dropped; }
// Stop receiving: leave the feed, let the worker finish what is queued, then exit.
@Override public void close() {
feed.remove(this);
synchronized (this) {
closed = true;
notifyAll();
}
}
void awaitDone() throws InterruptedException { if (worker != null) worker.join(); }
}
// The subject. Publishing costs one cheap offer per subscriber, however slow they are.
final class PriceFeed {
private final List<Subscription> subscriptions = new CopyOnWriteArrayList<>();
private long seq;
Subscription subscribe(TickListener listener, int capacity) {
Subscription s = new Subscription(listener, capacity, this);
subscriptions.add(s);
return s;
}
void remove(Subscription s) { subscriptions.remove(s); }
// synchronized: numbering and offering happen together, so every mailbox receives ticks
// in seq order even with several publishing threads.
synchronized void publish(String symbol, double price) {
Tick t = new Tick(++seq, symbol, price);
for (Subscription s : subscriptions) s.offer(t);
}
}
// A listener that notices gaps. Because ticks carry seq, a subscriber that fell behind knows
// it did, and can show "stale" or fetch a snapshot instead of silently showing old data.
final class Chart implements TickListener {
private long lastSeq;
public void onTick(Tick t) {
if (t.seq() > lastSeq + 1) System.out.println("chart: missed " + (t.seq() - lastSeq - 1) + " ticks");
System.out.printf("chart: #%d %s %.2f%n", t.seq(), t.symbol(), t.price());
lastSeq = t.seq();
}
}
class Main {
public static void main(String[] args) throws InterruptedException {
PriceFeed feed = new PriceFeed();
List<Long> seen = new ArrayList<>(); // only touched by the audit worker thread
Subscription audit = feed.subscribe(t -> seen.add(t.seq()), 100); // big mailbox
Subscription chart = feed.subscribe(new Chart(), 2); // tiny mailbox, slow screen
// Five ticks arrive before either subscriber has run at all: the worst case for a slow one.
// (Workers start after publishing only so this demo prints the same thing every time.)
for (int i = 1; i <= 5; i++) feed.publish("ACME", 100 + i);
audit.start();
audit.close();
audit.awaitDone();
System.out.println("audit saw: " + seen); // audit saw: [1, 2, 3, 4, 5]
chart.start();
chart.close();
chart.awaitDone(); // chart: missed 3 ticks, then #4 and #5
System.out.println("chart dropped: " + chart.dropped()); // chart dropped: 3
}
}
  • The feed never runs listener code. publish numbers the tick and calls offer on each subscription, which only touches a queue. A stuck listener blocks its own worker and nothing else.
  • Every bounded queue needs an overflow policy, and it’s a business decision. Dropping the oldest is right for prices, where only the latest value matters. Making the publisher wait is right when nothing may be lost and the publisher can slow down, like a file import. (Making it wait is called backpressure.) An order feed can do neither. That’s the point where an in-memory Observer should become a durable message queue. An unbounded queue is never right. It just delays the failure until memory runs out.
  • Sequence numbers make loss visible. Every tick carries seq, assigned under the feed’s lock. So each mailbox receives ticks in order, and a subscriber can tell exactly how many it missed. The chart can show “stale”, or fetch a fresh snapshot, instead of quietly drawing old data.
  • The slow subscriber pays for its own slowness. With drop-oldest, the audit log and the ticker see every tick. The chart sees the latest ones. Nobody else’s experience depends on the slowest screen.
  • Closing drains, then stops. close() leaves the feed first (so nothing new arrives), then lets the worker finish what’s queued. Removing under the feed’s lock is what lets Go close the channel safely.
  • The libraries already exist. .NET’s Channel.CreateBounded with DropOldest, Python’s deque(maxlen=...), Go’s buffered channels and Java’s reactive streams (Flow, Reactor’s onBackpressureLatest) are this design. Knowing the shape lets you pick the right one.
Interviewer

One client on a bad connection is making everyone’s prices late. Fix it.

What they are testing

Whether you see that synchronous notification makes everyone as slow as the slowest subscriber. Whether you give each subscriber its own bounded mailbox. And whether you pick an overflow policy with a reason (“only the latest price matters, so drop the oldest”). The red flag is an unbounded queue, or a new thread per tick.

Say this out loud

Each subscriber gets its own bounded mailbox, so the feed never waits on anyone. When a mailbox is full, we drop the oldest tick, because for prices only the latest matters. Sequence numbers let the subscriber know it skipped some.

Use Observer when the subject shouldn’t know its consumers, when consumers come and go, or when there are several of them owned by different people. Keep a direct call when there is one consumer that is part of the same operation, and especially when its failure should fail the operation.

Language Usual form Built in
Java Listener interface or lambda, CopyOnWriteArrayList PropertyChangeListener, java.util.concurrent.Flow
Python A list of callables Signals in frameworks (Django, Blinker)
C# event with += / -=, or IObservable<T> event, IObservable<T>, System.Threading.Channels
Go Function values in a map, or channels Channels, context for cancellation
TypeScript A Set of callbacks, subscribe returns an unsubscribe function DOM addEventListener, Node EventEmitter, RxJS
Pattern Looks like Observer because The difference
Publish-subscribe Both send events to many receivers Observer is in-process and the subject holds the list. Pub-sub puts a broker between them, so they don’t know each other and needn’t run at the same time.
Mediator Both decouple objects A mediator coordinates two-way conversations between peers and contains logic. A subject only announces.
Callback A listener is a callback A callback usually has one receiver for one operation. Observer has many receivers for a stream of events.
Event sourcing Both are built on events Event sourcing stores events as the source of truth. Observer events are notifications and are gone once delivered.

On this site:

  • Parking lot: display boards subscribe to parking events.
  • LRU cache: removal listeners are told why each entry left.
  • Logging framework: appenders receive each record, which is Observer with filtering, and async appenders are the Hard level’s mailbox.
  • Elevator: floor displays react to the car moving.

In real systems: every UI framework’s event system, React state subscriptions, database change streams, webhooks (Observer across the internet), and market data fan-out.

Interviewer asksHow do you avoid memory leaks with observers?

Make subscribe return a handle that unsubscribes, and tie it to the listener’s lifetime: dispose it when the screen closes or the board is replaced. Weak references are a fallback, not a fix: the listener then disappears at a time the garbage collector picks. In code review, look for subscribe calls with nothing that ever removes them.

Interviewer asksTwo threads change the subject at once. What can listeners see?

If you notify outside the lock (which you should, to avoid deadlocks), events from the two changes can arrive in either order. Carry totals rather than deltas, and a sequence number taken under the lock, so listeners can ignore anything older than what they’ve already seen. If strict order matters, deliver through one queue per listener, as in the Hard level.

Interviewer asksWhen would you replace Observer with a message queue?

When a consumer is in another process. When events must survive a crash. Or when nothing may be dropped, and the publisher can’t slow down. The queue adds durability, and lets the two sides run at different times. It costs a network hop, operations work, and usually duplicate delivery. So consumers must be idempotent.

Interviewer asksShould listeners be notified in a particular order?

Design so they don’t need to be. Say the board must update before metrics. That’s a hidden dependency between two listeners that are supposed to be independent. Where order truly matters, make it explicit. Use one listener that calls the steps in order. Or make priorities a documented feature, not an accident of the list.

Decision We chose We gave up
Coupling The subject knows one listener interface Reading the subject no longer shows what happens next
What counts as news The subject decides (empty to available) Listeners can’t ask for every change
Event shape Immutable value with totals and a sequence number Small events; they grow as listeners need more
Failures Catch and log per listener Listeners can’t fail or veto the operation
Leaks subscribe returns an unsubscribe handle Nothing; it’s the fix
Threads Notify outside the lock, over a snapshot Strict ordering across concurrent changes
Slow listeners A bounded mailbox each, drop the oldest Delivery when publish returns, and every tick for slow ones