Behavioral · Gang of Four
Observer
Observer is a behavioral pattern for one-to-many notification. A subject announces an event; listeners subscribe. The subject never imports the listener classes by name.
Example: a goal is scored. The scoreboard must update, the stats store must write a row, and a clipper may start. If Match calls all three by name, the next listener (SMS, replay camera) still edits Match. If Match only announces “goal scored,” a new listener is a new GoalObserver class.
The subject never names the listeners
Observer is a that keeps a list of observers and notifies them when something happens. In-process pub/sub is Observer with topic strings. The HLD pub/sub case study is brokers and delivery guarantees — keep broker delivery guarantees on the system-design page, not in this LLD note.
Read the diagram definition
flowchart LR
M[Match] -->|onGoal| S[Scoreboard]
M -->|onGoal| T[StatsStore]
M -->|onGoal| C[Clipper]
M -.->|does not import| X[next listener]Name the event
Goal, OrderPaid, SpotOccupied — a fact, not a procedure.
Hold Observer, not concretes
subscribe / unsubscribe / notify. Match never imports Scoreboard.
Listeners are cheap to add
A new Clipper implements the interface. Match does not change.
Observer vs Mediator
Broadcast news versus traffic control
Observer is one-to-many news: the subject does not coordinate the listeners, and the listeners do not need to talk to each other. Mediator is a hub among peers that must. If widgets enable each other, use Mediator. If many objects only need to hear “order paid,” use Observer.
Observer
Match ──notifies──> A, B, C. The is Goal. Scoreboard does not ask Stats to update.
Mediator
Name, Date, OK all talk through BookingDialog. The hub enables OK when two fields agree. That is not a Goal event.
Java: Match does not name Scoreboard
Prefer CopyOnWriteArrayList (or a synchronized snapshot) if subscribe and publish can race. On a single-threaded Match, a plain list plus List.copyOf is enough — state that assumption clearly. Isolate listener failures so one bad StatsStore does not silence Scoreboard. Pair subscribe with unsubscribe on close. Keep the event small. A notification fan-out is the same shape.
interface GoalObserver {
void onGoal(Goal goal);
}
class Match {
// Single-threaded Match: ArrayList + snapshot is fine.
// Concurrent subscribe/publish: use CopyOnWriteArrayList instead.
private final List<GoalObserver> observers = new CopyOnWriteArrayList<>();
void subscribe(GoalObserver o) { observers.add(o); }
void unsubscribe(GoalObserver o) { observers.remove(o); }
void score(Player scorer, int minute) {
Goal goal = new Goal(scorer, minute);
for (GoalObserver o : observers) {
try {
o.onGoal(goal);
} catch (RuntimeException e) {
// Failure isolation: one bad listener must not silence the rest.
log.warn("observer failed", e);
}
}
}
}
class Scoreboard implements GoalObserver {
public void onGoal(Goal goal) { /* redraw */ }
}
class StatsStore implements GoalObserver {
public void onGoal(Goal goal) { /* insert row */ }
}Notify under a lock, listener calls back
Forgotten unsubscribe
The subject imports every listener
If Match has fields for Scoreboard, Stats, and Sms, the next listener still edits Match. Depend on the interface.
Mixing HLD pub/sub into an LLD interview
Partitions, at-least-once, and consumer groups are the system-design note. Here you are naming an in-process list and an event type.
Snapshot, notify, isolate failures
The event is a fact: Goal(scorer, minute). Listeners must not need the whole Match. If Scoreboard calls match.getEverything(), you leaked the subject. Keep payloads small so notify stays cheap and lock-free.
Subscribe
Add to the list. Pair with a close() that unsubscribes.
Snapshot / COW
CopyOnWriteArrayList, or List.copyOf under a lock, if threads share Match.
Notify
Call onGoal(goal). Isolate failures. Do not hold the match lock.
Java: order-paid is the same shape
Email, ledger, and analytics subscribe. Order does not import Emailer. A new SMS listener is a new class.
interface OrderPaidObserver {
void onPaid(OrderPaid event);
}
class Order {
private final List<OrderPaidObserver> observers = new ArrayList<>();
void subscribe(OrderPaidObserver o) { observers.add(o); }
void markPaid(Instant at) {
OrderPaid event = new OrderPaid(id, at);
for (OrderPaidObserver o : List.copyOf(observers)) o.onPaid(event);
}
}In-process vs broker
This page is a list in memory. Kafka partitions and consumer groups live on the system-design pub/sub note. If the interviewer wants both, draw the in-process Observer first, then say the broker is the same idea across machines.
Synchronous notify
Default is sync: a slow StatsStore stalls the goal. If that matters, queue the event. Do not pretend Match is async because you named Observer.
How to explain this in an interview
“Match depends on GoalObserver, not Scoreboard. A new listener is a new class.” Then mention CopyOnWrite (or a single-threaded assumption), unsubscribe on close, and failure isolation so they know you have seen the sharp edges. Widgets that enable each other are still Mediator, not a Goal event. Keep the event small: scorer and minute, not the whole Match.
Brokers, partitions, and at-least-once delivery stay on the system-design note. Here the list is in process. Notify is synchronous unless you queue; a slow StatsStore stalls the goal.
Nearby in LLD