Observer and Mediator

Topics Covered

The Observer Pattern

The Core Interfaces

A Concrete Example: Stock Ticker

Push vs Pull Models

Event-Driven Communication

The EventEmitter Pattern

Multiple Listeners Per Event

Observer vs EventEmitter

Execution Order and Error Handling

Real-World Event Systems

The Mediator Pattern

Mediator Structure

Concrete Mediator: Chat Room

Beyond Chat: Air Traffic Control

When the Mediator Grows Too Large

Reducing Direct Dependencies

Observer vs Mediator

When to Use Each

Combining the Two

Side-by-Side Summary

Practice Problems

Every real system has objects that need to react when something else changes. A stock price updates and five different displays need to refresh. A user clicks a button and three different modules need to respond. The naive approach is to have the changing object call each dependent object directly: but this creates tight coupling. The stock ticker must know about every display. Adding a new display means modifying the ticker. This is where the Observer pattern saves you.

The Observer pattern defines a one-to-many dependency between objects. One object (the subject) maintains a list of dependents (the observers). When the subject's state changes, it notifies all registered observers automatically. The subject does not know what the observers do with the notification: it simply broadcasts the change.

Subject notifying multiple observers when state changes

The Core Interfaces

The pattern requires two contracts. The subject needs methods to manage its observer list and broadcast changes. Each observer needs a method to receive updates.

python
1from abc import ABC, abstractmethod
2
3class Observer(ABC):
4    @abstractmethod
5    def update(self, data):
6        pass
7
8class Subject(ABC):
9    def __init__(self):
10        self._observers = []
11
12    def subscribe(self, observer: Observer):
13        self._observers.append(observer)
14
15    def unsubscribe(self, observer: Observer):
16        self._observers.remove(observer)
17
18    def notify(self):
19        for observer in self._observers:
20            observer.update(self)

The subject holds a list of observers but never inspects them. It calls update() and moves on. This is the key insight: the subject is completely decoupled from what happens after notification.

A Concrete Example: Stock Ticker

A stock exchange tracks prices. Multiple displays, a dashboard, a mobile alert system, a logging service, need to react when prices change. Without Observer, the exchange would import and call each display class directly. With Observer, displays register themselves and the exchange broadcasts blindly.

python
1class StockExchange(Subject):
2    def __init__(self):
3        super().__init__()
4        self._prices = {}
5
6    def set_price(self, symbol: str, price: float):
7        self._prices[symbol] = price
8        self.notify()
9
10    def get_price(self, symbol: str) -> float:
11        return self._prices.get(symbol, 0.0)
12
13class DashboardDisplay(Observer):
14    def update(self, subject):
15        price = subject.get_price("AAPL")
16        print(f"Dashboard: AAPL is now ${price:.2f}")
17
18class AlertService(Observer):
19    def __init__(self, threshold: float):
20        self._threshold = threshold
21
22    def update(self, subject):
23        price = subject.get_price("AAPL")
24        if price > self._threshold:
25            print(f"ALERT: AAPL exceeded ${self._threshold:.2f}")

Adding a new observer, say, a CSV logger, requires zero changes to StockExchange. You create the logger class, implement update(), and call subscribe(). The exchange never knows it exists.

Key Insight

Observer is fundamentally a decoupling mechanism. The subject broadcasts without knowing who listens. Observers react without knowing who else is reacting. This separation means you can add, remove, or replace observers at runtime without modifying the subject. In interviews, when you hear 'notify multiple components when something changes,' Observer is your first tool.

Push vs Pull Models

In the push model, the subject sends the changed data directly in the update() call: observer.update(symbol="AAPL", price=152.30). The observer gets exactly what it needs without querying back.

In the pull model, the subject sends a reference to itself: observer.update(self). The observer queries the subject for whatever data it needs. This is more flexible because different observers can pull different data, but requires the observer to know the subject's interface.

Most real implementations use the push model for simple cases (one piece of data changes) and the pull model when the subject has rich state that different observers care about differently.

The Observer pattern notifies observers about state changes, but it ties observers to a specific subject. Event-driven communication takes decoupling further: publishers emit named events, and subscribers register handlers for events they care about. The publisher does not know who is listening. The subscriber does not know who is emitting. They agree only on the event name and data shape.

This is the architecture behind every GUI framework, every Node.js application, and every browser DOM interaction. When you click a button, you do not call a function on the dialog box. The button emits a "click" event. Anything that registered a handler for "click" on that button responds.

Publish-subscribe event lifecycle from emission to handler execution

The EventEmitter Pattern

An EventEmitter is a concrete implementation of event-driven communication. It manages a mapping from event names to lists of handler functions. Three operations define it:

python
1class EventEmitter:
2    def __init__(self):
3        self._handlers = {}
4
5    def on(self, event: str, handler):
6        if event not in self._handlers:
7            self._handlers[event] = []
8        self._handlers[event].append(handler)
9
10    def off(self, event: str, handler):
11        if event in self._handlers:
12            self._handlers[event].remove(handler)
13
14    def emit(self, event: str, data=None):
15        for handler in self._handlers.get(event, []):
16            handler(data)

on() registers a handler for an event name. off() removes it. emit() fires the event, calling every registered handler with the event data. The emitter does not know what the handlers do. The handlers do not know about each other.

Multiple Listeners Per Event

A single event can trigger many handlers. When a user places an order, the system emits "order_placed". The inventory service decrements stock. The email service sends a confirmation. The analytics service logs the event. The notification service pushes to the mobile app.

python
1order_system = EventEmitter()
2
3def update_inventory(order):
4    print(f"Reducing stock for {order['item']}")
5
6def send_confirmation(order):
7    print(f"Emailing {order['email']}")
8
9def log_analytics(order):
10    print(f"Logging order #{order['id']}")
11
12order_system.on("order_placed", update_inventory)
13order_system.on("order_placed", send_confirmation)
14order_system.on("order_placed", log_analytics)
15
16# One emit triggers all three handlers
17order_system.emit("order_placed", {
18    "id": 42,
19    "item": "Laptop",
20    "email": "[email protected]"
21})

Adding a new reaction to order placement means writing a handler function and calling on("order_placed", new_handler). No existing code changes.

Observer vs EventEmitter

The Observer pattern and EventEmitter solve the same problem, notifying dependents of changes, but differ in structure. In Observer, observers register with a specific subject and receive all its notifications. In EventEmitter, handlers register for specific named events, so they only receive events they care about.

Observer works well when a subject has one kind of state change. EventEmitter works better when a single object produces many different kinds of events, user login, user logout, user profile update, user deletion, and different handlers care about different events.

Execution Order and Error Handling

Handlers execute in registration order. If handler A is registered before handler B, A runs first. This is a synchronous process: emit() blocks until all handlers complete. If a handler throws an exception, subsequent handlers may not run (depending on implementation).

Production event systems address this with two strategies. Error isolation wraps each handler call in a try-catch so one failing handler does not block others. Async emission runs handlers independently, often in separate threads or event loop ticks, so slow handlers do not delay fast ones. Node.js EventEmitter, for example, is synchronous by default but developers commonly use process.nextTick() or setImmediate() to defer heavy handlers.

Real-World Event Systems

The EventEmitter pattern is everywhere. In the browser, element.addEventListener("click", handler) is an EventEmitter. In Node.js, streams, HTTP servers, and file watchers all extend EventEmitter. In Python, Django's signals framework and PyQt's signal-slot mechanism follow the same idea. In Java, Swing listeners and Spring's ApplicationEvent system are EventEmitter variants.

The common thread is decoupled communication through shared event contracts. The publisher defines what events it emits and what data they carry. Subscribers agree to handle that contract. Neither side knows or cares about the other.

The Observer pattern decouples a subject from its observers, but each observer still communicates independently. When objects need to coordinate with each other, not just react to changes, you need something different. The Mediator pattern introduces a central object that encapsulates how a set of objects interact. Instead of objects communicating directly, they send messages through the mediator, which decides how to route and handle them.

Think of a chat room. Users do not send messages to each other directly. They send messages to the chat room, and the chat room delivers them to the right recipients. Adding or removing a user does not affect any other user: they all talk to the room, not to each other.

Mediator chat room routing messages between participants

Mediator Structure

The mediator holds references to all participating objects (called colleagues). Each colleague holds a reference only to the mediator: never to other colleagues. When a colleague wants to interact with another, it tells the mediator, and the mediator routes the request.

python
1from abc import ABC, abstractmethod
2
3class ChatMediator(ABC):
4    @abstractmethod
5    def send_message(self, message: str, sender):
6        pass
7
8    @abstractmethod
9    def add_user(self, user):
10        pass
11
12class User:
13    def __init__(self, name: str, mediator: ChatMediator):
14        self.name = name
15        self._mediator = mediator
16        self._mediator.add_user(self)
17
18    def send(self, message: str):
19        self._mediator.send_message(message, self)
20
21    def receive(self, message: str, sender_name: str):
22        print(f"[{self.name}] Message from {sender_name}: {message}")

Notice that User never imports or references another User. It knows only about the ChatMediator interface. This is the decoupling at work.

Concrete Mediator: Chat Room

The concrete mediator implements the routing logic. In a chat room, "send a message" means delivering it to every user except the sender.

python
1class ChatRoom(ChatMediator):
2    def __init__(self):
3        self._users = []
4
5    def add_user(self, user: User):
6        self._users.append(user)
7
8    def send_message(self, message: str, sender: User):
9        for user in self._users:
10            if user != sender:
11                user.receive(message, sender.name)
12
13# Usage
14room = ChatRoom()
15alice = User("Alice", room)
16bob = User("Bob", room)
17charlie = User("Charlie", room)
18
19alice.send("Hello everyone!")
20# Bob receives: [Bob] Message from Alice: Hello everyone!
21# Charlie receives: [Charlie] Message from Alice: Hello everyone!

The mediator can implement any routing logic: broadcast to all, send to a specific user, filter by channel, or even transform the message before delivery. The colleagues do not care. They call send() and the mediator handles the rest.

Interview Tip

In interviews, reach for the Mediator pattern when you see multiple objects that need coordinated communication: chat systems, air traffic control, UI form validation where fields depend on each other, or workflow engines where steps trigger other steps. The interviewer wants to see that you recognize when direct object-to-object communication creates an unmaintainable web of dependencies.

Beyond Chat: Air Traffic Control

A control tower is a mediator. Planes do not communicate with each other about landing order, runway availability, or altitude changes. They report to the tower, and the tower coordinates. If Plane A requests landing, the tower checks runway status, other planes' positions, and weather: then instructs Plane A where and when to land. No plane needs to know about any other plane.

This is the pattern's strength: centralized coordination logic. When the rules for interaction change, a new runway opens, priority rules change for emergencies, you modify only the mediator. The planes (colleagues) remain unchanged.

When the Mediator Grows Too Large

The mediator's centralization is both its strength and its risk. If every interaction between every object passes through the mediator, the mediator accumulates all the complexity that was previously spread across the objects. It can become a "god object": a massive class that knows too much and does too much.

The defense is to keep mediators focused. A ChatRoom mediates chat. A ControlTower mediates flight coordination. Do not create a SystemMediator that mediates everything. If your mediator class exceeds 200-300 lines, it is likely handling concerns that belong in separate mediators.

Both Observer and Mediator exist to solve the same root problem: direct dependencies between objects create systems that are hard to change. To understand why, consider what happens without these patterns.

Imagine five components that all need to communicate with each other. Component A calls B and C directly. B calls A, D, and E. C calls A and E. Each object imports and references the others. The number of direct connections is N times (N minus 1) divided by 2: for five objects, that is 10 direct connections. Each connection is a point of coupling. Change the interface of one object and you must update every object that calls it.

Mesh topology with direct coupling versus star topology with mediator decoupling

This is a mesh topology: every node talks to every other node. It works for two or three objects. At ten objects, you have 45 direct connections. At twenty, 190. The system becomes impossible to understand, test, or modify without breaking something.

The Observer pattern replaces some of these connections with a broadcast mechanism. Instead of A calling B, C, and D when its state changes, A broadcasts a notification. B, C, and D listen independently. A's direct dependency count drops from 3 to 0. The total connection count drops because all the "A changed" dependencies become subscriptions to A's notification rather than direct imports.

The Mediator pattern goes further by replacing the entire mesh with a star topology. Every object connects only to the central mediator. N objects need exactly N connections instead of N times (N minus 1) divided by 2. The mediator becomes the single point of coordination. Adding a new object means one new connection to the mediator. Removing an object means one disconnection. No other object is affected.

The trade-off is real. The mediator absorbs the complexity that was previously distributed across all objects. If the interaction rules are simple, the mediator stays small and the trade-off is clearly worthwhile. If the interaction rules are complex, the mediator can bloat into a god object that is harder to maintain than the original mesh. The guideline is to use a mediator when the interaction logic is the complicated part, chat routing, workflow coordination, form validation with interdependent fields, and to avoid it when objects simply need to be notified of changes, where Observer is sufficient.

In interviews, framing your answer in terms of dependency topology scores well. Saying "this design uses a star topology through a mediator, reducing N-squared connections to N" demonstrates that you think about coupling quantitatively, not just intuitively. It shows you understand that design patterns are not arbitrary rules but concrete strategies for managing complexity as systems grow.

Both Observer and Mediator reduce coupling between objects. Both let you add new participants without modifying existing ones. But they solve different problems and interviewers expect you to know the distinction.

Observer is decentralized. The subject broadcasts a notification. Each observer independently decides how to react. There is no coordination between observers: they do not know about each other and they do not influence each other's behavior. The subject does not control what observers do. It says "my state changed" and walks away.

Mediator is centralized. All interaction logic lives in the mediator. Colleagues send requests to the mediator, and the mediator decides what happens: who receives what, in what order, under what conditions. The mediator actively coordinates. It does not just relay notifications; it implements rules.

When to Use Each

Use Observer when the interaction is simple notification. A value changed and multiple components need to react independently. Examples: UI data binding, event logging, cache invalidation, real-time dashboards. Each observer acts on its own. No observer needs to know what other observers are doing.

Use Mediator when the interaction involves complex coordination. Multiple objects need to communicate with each other, and the communication has rules, conditions, or ordering requirements. Examples: chat rooms (routing to specific users), air traffic control (sequencing landings), form validation (field A enables field B which disables field C), workflow engines (step completion triggers next step).

The decision framework is straightforward: if removing one observer would not affect any other observer's behavior, use Observer. If removing one participant changes how other participants interact, use Mediator.

Common Pitfall

A common interview mistake is treating Observer and Mediator as interchangeable because both reduce coupling. They reduce different kinds of coupling. Observer eliminates the subject's knowledge of its dependents. Mediator eliminates colleagues' knowledge of each other. Conflating them signals shallow understanding of both patterns.

Combining the Two

Real systems frequently combine both patterns. A chat application might use a ChatRoom mediator to coordinate message routing between users, while each User object uses the Observer pattern so that the UI, notification service, and message logger can independently react when the user receives a message.

The mediator handles inter-object coordination (routing messages between users). The observers handle intra-object notification (reacting when a single user's state changes). These are complementary, not competing, purposes.

Side-by-Side Summary

AspectObserverMediator
TopologyOne-to-many broadcastStar (all through center)
Logic locationDistributed in observersCentralized in mediator
CoordinationNone, observers act independentlyActive, mediator routes and decides
Best forState change notificationsComplex multi-object interactions
RiskObserver leaks (memory)God object (bloated mediator)
Coupling reducedSubject to observersColleagues to each other

Practice Problems

Apply what you have learned about Observer and Mediator by implementing these systems from scratch.

Loading problem...

Loading problem...

Loading problem...