Vasu Patel logoVasu Patel
← Patterns of Good Design
All chapters in this track
  1. 01Chapter 1: Why Patterns Exist — Vocabulary, Not a Checklist
  2. 02Chapter 2: Patterns and SOLID: How They Relate
  3. 03Chapter 3: Creational Patterns I: Factory Method & Abstract Factory
  4. 04Chapter 4: Creational Patterns II: Builder, Singleton, Prototype
  5. 05Chapter 5: Structural Patterns I: Adapter & Facade
  6. 06Chapter 6: Structural Patterns II: Decorator & Proxy
  7. 07Chapter 7: Structural Patterns III: Composite, Bridge & Flyweight
  8. 08Chapter 8: Behavioral Patterns I: Strategy & Template Method
  9. 09Chapter 9: Behavioral Patterns II: Observer & Mediator
  10. 10Chapter 10: Behavioral Patterns III: Command & Memento
  11. 11Chapter 11: Behavioral Patterns IV: State & Chain of Responsibility
  12. 12Chapter 12: Behavioral Patterns V: Iterator & Visitor
  13. 13Chapter 13: Anti-Patterns and Overuse: Recognizing "Pattern Fever"
  14. 14Chapter 14: How Dynamic and Functional Languages Solve These Problems Differently
  15. 15Chapter 15: Architectural Patterns I: MVC/MVVM & Layered/Hexagonal Architecture
  16. 16Chapter 16: Architectural Patterns II: Event-Driven Architecture & CQRS
  17. 17Chapter 17: Architectural Patterns III: The Repository Pattern
  18. 18Chapter 18: Recognizing Which Pattern Fits an Interview Design Prompt

Chapter 9 of 18

Chapter 9: Behavioral Patterns II: Observer & Mediator

Observer

The problem it solves: one object's state change needs to notify an open-ended set of other objects, without the changing object needing to know who those observers are or how many there are.

Before — the changing object is hard-wired to every interested party:

class StockPrice {
  private price = 0;
  constructor(private emailAlert: EmailAlert, private smsAlert: SmsAlert) {} // hard-wired to specific observers

  setPrice(price: number): void {
    this.price = price;
    this.emailAlert.notify(price); // adding a new kind of alert means editing this class
    this.smsAlert.notify(price);
  }
}

After — Observer decouples the subject from an open-ended, dynamic list of observers:

interface Observer {
  update(price: number): void;
}

class StockPrice {
  private observers: Observer[] = [];
  private price = 0;

  subscribe(observer: Observer): void { this.observers.push(observer); }
  unsubscribe(observer: Observer): void {
    this.observers = this.observers.filter(o => o !== observer);
  }

  setPrice(price: number): void {
    this.price = price;
    for (const observer of this.observers) {
      observer.update(price); // StockPrice has no idea what kind of observer this is, or how many exist
    }
  }
}

class EmailAlert implements Observer {
  update(price: number): void { console.log(`Email: price is now ${price}`); }
}
class LoggingObserver implements Observer {
  update(price: number): void { console.log(`Log: price changed to ${price}`); }
}

const stock = new StockPrice();
stock.subscribe(new EmailAlert());
stock.subscribe(new LoggingObserver());
stock.setPrice(150); // both observers notified; StockPrice never changed to support this
from typing import Protocol

class Observer(Protocol):
    def update(self, price: float) -> None: ...

class StockPrice:
    def __init__(self) -> None:
        self._observers: list[Observer] = []
        self._price = 0.0

    def subscribe(self, observer: Observer) -> None:
        self._observers.append(observer)

    def set_price(self, price: float) -> None:
        self._price = price
        for observer in self._observers:
            observer.update(price)

[IN PRACTICE] This is the exact mechanism behind reactive UI frameworks, pub/sub messaging systems, and event listeners in general — addEventListener in the browser DOM is Observer, applied at a platform level. Recognizing Observer's shape in these familiar tools is often the fastest way to internalize the pattern.

When to use it: an open-ended, potentially changing set of parties needs to react to one object's state changes, and that object shouldn't need to know who they are.

When to avoid it: if there's exactly one fixed listener that will never change, direct method calls are simpler than the subscribe/notify machinery — Observer earns its keep specifically when the set of interested parties is dynamic or plural.

Complexity cost: moderate — notification order isn't guaranteed unless deliberately controlled, debugging "who reacted to this and why" gets harder as observer count grows, and a classic memory-leak risk exists if observers subscribe but are never unsubscribed (especially in long-lived subjects) — worth explicit attention in any real implementation.

Mediator

The problem it solves: many objects need to communicate with each other, and letting them reference each other directly creates a tangled web of dependencies where every object needs to know about every other — Mediator centralizes that communication through one coordinating object.

Before — objects reference each other directly, creating a tangled web:

class Button {
  constructor(private textField: TextField, private submitLabel: Label) {}
  onClick(): void {
    this.textField.clear();
    this.submitLabel.setText("Submitted!");
  }
}
class TextField {
  constructor(private charCountLabel: Label) {}
  onChange(text: string): void {
    this.charCountLabel.setText(`${text.length} chars`);
  }
  clear(): void { console.log("Cleared"); }
}
// Every UI component needs a direct reference to every other component it affects —
// adding a new component means threading references through the whole web.

After — Mediator centralizes coordination, so components only know the mediator:

interface Mediator {
  notify(sender: string, event: string, payload?: unknown): void;
}

class FormMediator implements Mediator {
  private textField = new TextField(this);
  private submitLabel = new Label();
  private charCountLabel = new Label();

  notify(sender: string, event: string, payload?: unknown): void {
    if (sender === "textField" && event === "change") {
      this.charCountLabel.setText(`${(payload as string).length} chars`);
    }
    if (sender === "button" && event === "click") {
      this.textField.clear();
      this.submitLabel.setText("Submitted!");
    }
  }
}

class TextField {
  constructor(private mediator: Mediator) {} // only knows the mediator, not other components
  onChange(text: string): void { this.mediator.notify("textField", "change", text); }
  clear(): void { console.log("Cleared"); }
}
class Label {
  setText(text: string): void { console.log(`Label: ${text}`); }
}
from typing import Protocol, Any

class Mediator(Protocol):
    def notify(self, sender: str, event: str, payload: Any = None) -> None: ...

class TextField:
    def __init__(self, mediator: Mediator) -> None:
        self._mediator = mediator

    def on_change(self, text: str) -> None:
        self._mediator.notify("textField", "change", text)

When to use it: a group of objects has many-to-many interdependencies that are becoming hard to trace or modify — centralizing coordination trades "many small dependencies scattered everywhere" for "one place that knows the interaction rules," which is easier to reason about even though that one place grows more complex.

When to avoid it: only a few objects interact in a simple, stable way — introducing a Mediator here just relocates simple direct calls into an indirection layer with no real payoff. Also watch for the Mediator itself becoming a God Object (OOP book, Chapter 12) if it accumulates too much unrelated coordination logic.

Complexity cost: the Mediator itself concentrates complexity that was previously distributed — a worthwhile trade only when the distributed version had actually become hard to follow; otherwise you've just moved the tangle into one file instead of removing it.

Observer vs. Mediator

[SENIOR INSIGHT] Both decouple objects from knowing about each other directly, but the shape differs: Observer is one-to-many, one direction — a subject broadcasts to observers who don't talk back through the same channel. Mediator is many-to-many, bidirectional — multiple objects both send and receive coordination through one central point. If you find a Mediator implementation where objects only ever broadcast outward with no coordination logic reacting to combinations of events, that's really Observer wearing a Mediator's name.

Common mistakes

  1. Subscribing to an Observer subject and never unsubscribing, causing a memory leak — especially dangerous when the subject outlives the observer's intended lifetime (e.g., a UI component destroyed while still subscribed to a global event source).
  2. Letting a Mediator grow into a God Object by routing unrelated concerns through it just because "it's already the central coordination point" — the same accretion pattern the OOP book's Chapter 12 describes.
  3. Using Mediator for a small, stable set of interactions that would be clearer as direct method calls — the indirection isn't earning its cost yet.
  4. Relying on Observer notification order without explicitly designing for it — if the order observers are notified in actually matters, that assumption needs to be made explicit and tested, not left implicit in list-registration order.

Discussion / interview questions

  1. "What problem does Observer solve, and what's a real-world example you've encountered?" Strong answer: Lets a subject notify an open-ended, dynamic set of interested parties without knowing who they are; names a familiar real example (DOM event listeners, a pub/sub message bus, a reactive framework's state subscriptions).

  2. "What's a common bug risk specific to the Observer pattern?" Strong answer: Memory leaks from observers that subscribe but are never unsubscribed, especially when the subject significantly outlives the observer.

  3. "How does Mediator differ from Observer, given both reduce direct coupling between objects?" Strong answer: Observer is one-to-many and one-directional (broadcast); Mediator is many-to-many and bidirectional (centralized coordination where objects both send and receive through the mediator).

  4. "What's the risk of overusing the Mediator pattern?" Strong answer: The Mediator itself can accumulate unrelated coordination logic and become a God Object (OOP book, Chapter 12) — complexity that was distributed gets concentrated, which is only a net win if the distributed version had genuinely become hard to follow.

Chapter summary / cheat sheet

  • Observer: one-to-many, one-directional notification; decouples a subject from a dynamic, open-ended set of listeners. Watch for unsubscribe/memory-leak risk.
  • Mediator: many-to-many, bidirectional coordination centralized in one object; trades distributed tangled dependencies for concentrated (but traceable) complexity in one place. Watch for it becoming a God Object.
  • Both patterns decouple objects from knowing about each other directly — the shape of the interaction (broadcast vs. centralized coordination) determines which fits.