Observer
markDelivered() started as two lines: set the status, save. Then it awarded loyalty
points. Then it asked the customer for a rating. Then it pinged analytics. Then it
updated the rider’s earnings, and sent the invoice email. Each one was a team adding one
line to a function owned by the orders team.
One afternoon the analytics endpoint timed out and threw. It was the third call of five. Rider earnings and the invoice email, calls four and five, never ran. 1,900 riders were underpaid that day because a dashboard was slow.
When an order is delivered, five other parts of the system need to react, and that list keeps growing. How would you arrange it so the order code does not have to list them, and one of them failing does not stop the others?
The idea
You tap the bell on a channel once. After that, every upload reaches you. The creator does not have your phone number and does not message subscribers one at a time. They publish, and the platform tells whoever asked to be told. You can turn the bell off whenever you like, and the creator never finds out.
Two roles. The subject is the thing that changes and announces it. An observer, also called a listener or subscriber, is anything that asked to be told.
Before: the subject knows everyone
class Loyalty {
award(orderId: number): string {
return `points for order ${orderId}`
}
}
class Ratings {
ask(orderId: number): string {
return `rating request for order ${orderId}`
}
}
class OrderService {
private readonly loyalty = new Loyalty()
private readonly ratings = new Ratings()
markDelivered(orderId: number): string[] {
return [this.loyalty.award(orderId), this.ratings.ask(orderId)]
// next sprint: earnings, analytics, invoice email...
}
}
console.log(new OrderService().markDelivered(42))
// > [ 'points for order 42', 'rating request for order 42' ]The arrows point the wrong way. Orders is the core of the business and it depends on
loyalty, ratings, and everything that will ever care about a delivery. Each new
subscriber is an edit to OrderService.
After: the subject announces, and listeners subscribe
type Listener<T> = (event: T) => void
class Subject<T> {
private readonly listeners = new Set<Listener<T>>()
subscribe(listener: Listener<T>): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
notify(event: T): void {
for (const listener of this.listeners) listener(event)
}
}
type OrderDelivered = { orderId: number; amount: number }
const orderDelivered = new Subject<OrderDelivered>()
orderDelivered.subscribe((event) => {
const points = Math.floor(event.amount / 10)
console.log(`Loyalty: ${points} points for order ${event.orderId}`)
})
const stopRatings = orderDelivered.subscribe((event) => {
console.log(`Ratings: ask about order ${event.orderId}`)
})
orderDelivered.notify({ orderId: 42, amount: 240 })
// > Loyalty: 24 points for order 42
// > Ratings: ask about order 42
stopRatings()
orderDelivered.notify({ orderId: 43, amount: 90 })
// > Loyalty: 9 points for order 43Subject is 12 lines and it is the whole pattern. It keeps a set of functions, adds to
it on subscribe, and calls each one on notify.
The order code now does one thing after a delivery: orderDelivered.notify(...). It does
not import loyalty or ratings. When a sixth team wants in, they call subscribe from
their own file. Nobody opens the orders code.
Two parts of the TypeScript deserve a second look.
Subject<T> is a generic class. T is the type of the event. new Subject<OrderDelivered>()
fixes it, and from then on TypeScript knows that event inside every listener has an
orderId and an amount. Try event.total and it will not compile. One class, reused
for every kind of event, fully typed each time.
subscribe returns a function. Calling it removes that listener. This is cleaner
than a separate unsubscribe(listener) method, because the caller does not have to hold
on to the original function to cancel it. React’s useEffect cleanup follows the same
convention.
One bad listener should not stop the rest
The version above still has the bug from the top of the page. If a listener throws, the
for loop ends and the listeners after it never run. A subject you would use in
production isolates them.
type Listener<T> = (event: T) => void
class Subject<T> {
private readonly listeners = new Set<Listener<T>>()
subscribe(listener: Listener<T>): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
notify(event: T): void {
for (const listener of this.listeners) {
try {
listener(event)
} catch (error) {
const reason = error instanceof Error ? error.message : 'unknown'
console.log(`a listener failed: ${reason}`)
}
}
}
}
const delivered = new Subject<number>()
delivered.subscribe(() => {
throw new Error('analytics timed out')
})
delivered.subscribe((orderId) => {
console.log(`Earnings updated for order ${orderId}`)
})
delivered.notify(42)
// > a listener failed: analytics timed out
// > Earnings updated for order 42Analytics failed and the rider was still paid.
The textbook version
An interviewer may draw it with interfaces and the words attach, detach and update. It is the same thing with more names.
interface Observer {
update(score: string): void
}
class Match {
private observers: Observer[] = []
attach(observer: Observer): void {
this.observers.push(observer)
}
detach(observer: Observer): void {
this.observers = this.observers.filter((other) => other !== observer)
}
setScore(score: string): void {
for (const observer of this.observers) observer.update(score)
}
}
class PhoneApp implements Observer {
constructor(private readonly owner: string) {}
update(score: string): void {
console.log(`${this.owner}'s phone: ${score}`)
}
}
const match = new Match()
const asha = new PhoneApp('Asha')
match.attach(asha)
match.attach(new PhoneApp('Ravi'))
match.setScore('IND 142/3')
// > Asha's phone: IND 142/3
// > Ravi's phone: IND 142/3
match.detach(asha)
match.setScore('IND 148/3')
// > Ravi's phone: IND 148/3Where you have already met it: button.addEventListener('click', handler) in the browser,
EventEmitter in Node, store.subscribe in Redux, and every observable in RxJS.
What goes wrong
The subject holds a reference to every listener, so a listener cannot be garbage
collected while it is subscribed. A screen that subscribes when it opens and never
unsubscribes when it closes stays in memory, and keeps running its handler, for the
life of the app. Open that screen 50 times and one event fires 50 handlers. Every
subscribe needs a matching call to the function it returned.
With direct calls you can read markDelivered and see what happens next. With observers
you see notify and have to search the codebase for who subscribed. This is the real
price of the pattern. Use it where the subject truly should not know its listeners, and
keep a direct call where two things are part of one operation and must succeed or fail
together.
In Observer, listeners register with the subject itself, inside one process, and
notify calls them directly. In publish and subscribe there is a broker in the middle,
publishers and subscribers know only the broker and a topic name, and delivery is
usually asynchronous and across services. When the listeners on this page become
separate services, the subject becomes a queue, and that is the
message queues page in the HLD section.
type Listener<T> = (event: T) => voidclass Subject<T>new Subject<OrderDelivered>()new Set<Listener<T>>()subscribe(...): () => voidfor (const listener of this.listeners)observers.filter((other) => other !== observer)Checkpoint
1. After moving to Observer, the referrals team wants to credit a bonus when an order is delivered. Which file do they edit?
2. A screen subscribes to a subject when it opens and does nothing when it closes. What happens over time?
3. In the basic Subject, the second of five listeners throws. What happens to listeners three to five?
Observer is for when one thing changes and several others need to react, and the thing that changed should not have to know who they are. The subject keeps a list of listeners, offers subscribe and unsubscribe, and calls each listener when something happens. So the order code announces order delivered, and loyalty, ratings and earnings each subscribe from their own modules, which reverses the dependency. In TypeScript I write it as a small generic Subject class holding a set of callbacks, where subscribe returns the function that unsubscribes. The risks are memory leaks from listeners that never unsubscribe, one failing listener stopping the others unless notify catches per listener, and control flow that is harder to trace. Across services the same idea becomes publish and subscribe with a message broker.
