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.

Your answer

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

The picture to keep
The bell icon

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.

In one line: One object announces that something happened. Everyone who subscribed is told. The announcer does not know who they are.

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 43

Subject 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.

Figure 1. The order code points at the subject and nothing else. Listeners point at the subject too, so adding a fourth is a change in the listener's own file.

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 42

Analytics 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/3

Where 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 listener that never leaves

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.

Flow you cannot follow

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.

Observer versus publish and subscribe

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.

TypeScript you just picked up
type Listener<T> = (event: T) => void
A generic function type. A callback that receives a T and returns nothing useful.
class Subject<T>
A generic class. T is chosen once per instance and checked everywhere it appears.
new Subject<OrderDelivered>()
Fixes T. Every listener on this subject now receives an OrderDelivered.
new Set<Listener<T>>()
A collection with no duplicates. Adding the same listener twice subscribes it once.
subscribe(...): () => void
A method that returns a function. Here, the function that undoes the subscription.
for (const listener of this.listeners)
for...of walks any iterable: arrays, Sets, Maps.
observers.filter((other) => other !== observer)
A new array without one element. Objects are compared by identity.

Checkpoint

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?

Say this in 60 seconds

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.

IndGeek provides solutions in the software field, and is a hub for ultimate Tech Knowledge.