Before you start
Observer is the pattern behind every event-driven system you'll ever work on. UI clicks. Django signals. RxJS streams. DOM events. Distributed message queues (in shape, if not exact implementation). Learn it here; you'll apply it forever.
The one-line version
Publisher notifies subscribers. Subscribers sign up and leave on their own. Publisher doesn't know what subscribers do; subscribers don't know about each other.
When Observer beats Facade
Both handle "one thing happens, N reactions". The difference:
- Facade (last lesson) — FIXED, ORDERED reactions. Login ALWAYS goes: auth → audit → session → analytics. Every reaction must run. Order matters.
- Observer — GROWING, INDEPENDENT reactions. Order
placed: today 4 reactions, tomorrow 6, next quarter
- Each reaction is a plug-in.
Facade is a design decision baked into your code. Observer is a coordination pattern that lets other people (or your future self) add reactions later.
Where Observer shows up
- Django signals (
post_save,pre_delete) — subscribers register with@receiver. - NestJS event emitter —
@OnEvent('order.placed'). - JavaScript's
addEventListener— the DOM's Observer. - RxJS Observables — reactive-style Observer.
- React state hooks — components subscribe to store changes.
- File watchers — react to file changes.
- Chat systems — every user's message triggers per-recipient reactions.
- CI systems — code push notifies test runners, linters, deployers.
The pattern is EVERYWHERE. The name changes; the shape doesn't.
The best-effort trap
Observer notifiers face one big choice: what if a subscriber throws?
- Best-effort (swallow) — other subscribers still run. Log the error. Nothing crashes.
- Strict (propagate) — the whole publish operation fails. Callers see the error.
Both are valid; different systems want different behavior. Django signals: strict by default. NestJS EventEmitter: best-effort. This lesson uses best-effort — the typical "send emails / update analytics / etc." case doesn't want one flaky subscriber to break everything.
In real code, ALWAYS log the swallowed errors. Silent failures are how systems slowly drift into being broken without anyone noticing.
The subscription-order trap
Observers usually run in the order they registered. But relying on this is fragile:
- Adding a new subscriber "just before" another one means you have to know the wiring order.
- Two subscribers both wanting to go first causes bugs that only show up at runtime.
If order MATTERS, you probably want Facade (explicit ordering) or a Mediator. Observer is best for reactions that are truly INDEPENDENT.
The mid-notification-mutation subtlety
If a subscriber unsubscribes ITSELF during
notification, and you're iterating self._subscribers
directly, you get "RuntimeError: list changed size
during iteration" or you silently skip some
subscribers.
Fix: iterate over a COPY of the subscribers list. Our
lesson does this via list(self._subscribers). Real
production code does the same.
Observer vs Command vs State
Three behavioral patterns that get mixed up:
- Observer — WHO REACTS when something happens
- Command (next lesson) — WHAT ACTION happens, as a first-class object
- State (final lesson) — HOW BEHAVIOR CHANGES based on the object's internal state
Different problems.
The decision cheat-sheet
| Situation | Pick |
|---|---|
| Fixed set of always-run reactions with order | Facade |
| Growing / dynamic reactions, independent | Observer |
| Cross-process events | Message queue (HLD) |
| Global state changes trigger updates | Observer (or Redux-style store) |
| Chat / two-way coordination | Mediator (not in this course) |