Chapter 05 · Article 29 of 55
Mediator Pattern
Intent: Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it let…
Article outline15 sections on this page+
Overview
Intent: Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
Category: Behavioural Design Pattern
Also Known As: Controller, Coordinator
Complexity: **
Frequency of Use: High (especially in UI frameworks, messaging systems, and orchestration layers)
The Mediator pattern introduces a central authority - the mediator - that coordinates communication between multiple objects (called colleagues). Instead of each object holding references to every other object it needs to communicate with, all objects communicate exclusively through the mediator. This transforms a complex many-to-many relationship graph into a simple star topology where the mediator sits at the hub.
The pattern embodies the principle of least knowledge (Law of Demeter) at an architectural level: objects only know about the mediator, never about each other's internal details or even existence.
Problem It Solves
Consider a system with N objects that need to communicate with each other. Without a mediator:
- N × (N-1) relationships exist - every object potentially talks to every other object
- Adding a new object requires modifying up to N-1 existing objects
- Removing an object risks breaking N-1 other objects
- Business logic for coordination is scattered across all participants
- Testing any single object requires mocking all its communication partners
Concrete scenario: A flight booking form has fields for departure city, arrival city, date, airline, and class. Changing the departure city must update available airlines, which updates available classes, which updates the price display. Without a mediator, each field widget holds references to every other widget it affects - creating brittle spaghetti coupling.
With a mediator, the N×(N-1) relationships collapse to N×1. Each object only knows the mediator. The mediator encapsulates the coordination logic in one place, making the system easier to understand, modify, and test.
When to Use / When NOT to Use
| When to Use | When NOT to Use |
|---|---|
| Multiple objects communicate in complex, hard-to-understand ways | Only 2-3 objects interact with simple, stable relationships |
| Reusing an object is difficult because it refers to many others | Communication patterns are unlikely to change |
| You want to customize interaction behaviour without subclassing many classes | The "mediation" logic is trivial (just forwarding calls) |
| Coordination logic changes frequently and should be centralized | Performance is critical and the indirection overhead matters |
| You need to decouple a set of classes that were designed to work together | Objects genuinely need direct references for tight real-time coupling |
| Event-driven architectures where producers shouldn't know consumers | The mediator would become a god object with thousands of lines |
| GUI form coordination where field changes cascade to other fields | Simple publish-subscribe suffices without routing logic |
Key Concepts & Theory
Mediator vs Direct Communication
In direct communication, Object A calls methods on Object B directly. A holds a reference to B, knows B's interface, and is tightly coupled to B's existence. If B changes or is removed, A breaks.
In mediated communication, Object A tells the mediator "event X happened." The mediator decides who needs to know and invokes the appropriate methods on the relevant colleagues. A never knows B exists.
Colleague Objects
Colleagues are the participants that communicate through the mediator. Each colleague:
- Holds a reference to the mediator (typically set via constructor injection)
- Notifies the mediator when its state changes
- Exposes methods the mediator can call to update it
- Never references other colleagues directly
Centralized Control
The mediator centralizes the "interaction protocol" - the rules governing who reacts to what and how. This makes the protocol:
- Visible - read one class to understand all interactions
- Modifiable - change coordination without touching colleagues
- Replaceable - swap mediator implementations for different behaviours
Event-Based Mediation
Modern implementations often use an event/message-based approach:
- Colleague emits a typed event/message to the mediator
- Mediator has registered handlers for each event type
- Handlers execute the coordination logic
- This decouples even the mediator from knowing colleague concrete types
Libraries like MediatR (.NET) and Guava EventBus (Java) formalize this approach.
ASCII Class Diagram
┌─────────────────────────────────────────────────────────────────┐
│ «interface» │
│ Mediator │
│─────────────────────────────────────────────────────────────────│
│ + notify(sender: Colleague, event: String): void │
└─────────────────────────┬───────────────────────────────────────┘
│ implements
▼
┌─────────────────────────────────────────────────────────────────┐
│ ConcreteMediator │
│─────────────────────────────────────────────────────────────────│
│ - colleagueA: ConcreteColleagueA │
│ - colleagueB: ConcreteColleagueB │
│ - colleagueC: ConcreteColleagueC │
│─────────────────────────────────────────────────────────────────│
│ + notify(sender: Colleague, event: String): void │
│ + registerColleague(colleague: Colleague): void │
└──────────┬──────────────┬───────────────────┬───────────────────┘
│ │ │
│ references │ references │ references
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ConcreteColleagueA│ │ConcreteColleagueB│ │ConcreteColleagueC│
└───────┬────────┘ └───────┬────────┘ └───────┬────────┘
│ │ │
└──────────────────┴───────────────────┘
│ extends
▼
┌─────────────────────────────────────┐
│ «abstract» Colleague │
│─────────────────────────────────────│
│ # mediator: Mediator │
│─────────────────────────────────────│
│ + setMediator(m: Mediator): void │
│ + changed(): void // calls │
│ mediator.notify(this, event) │
└─────────────────────────────────────┘
Pseudocode Implementation
Example 1: Chat Room
// Mediator interface
interface ChatMediator {
sendMessage(message: String, sender: User, recipient: String?)
addUser(user: User)
}
// Concrete Mediator
class ChatRoom implements ChatMediator {
private users: Map<String, User> = {}
addUser(user: User) {
users[user.name] = user
user.setMediator(this)
}
sendMessage(message: String, sender: User, recipient: String?) {
if recipient != null {
// Direct message - route to specific user
if users.contains(recipient) {
users[recipient].receive(message, sender.name)
}
} else {
// Broadcast - send to all except sender
for each (name, user) in users {
if name != sender.name {
user.receive(message, sender.name)
}
}
}
}
}
// Colleague
abstract class User {
protected mediator: ChatMediator
public name: String
constructor(name: String) { this.name = name }
setMediator(m: ChatMediator) { this.mediator = m }
send(message: String, to: String?) {
mediator.sendMessage(message, this, to)
}
abstract receive(message: String, from: String)
}
// Concrete Colleague
class ChatUser extends User {
receive(message: String, from: String) {
print("[{this.name}] Message from {from}: {message}")
}
}
// Usage
room = new ChatRoom()
alice = new ChatUser("Alice")
bob = new ChatUser("Bob")
charlie = new ChatUser("Charlie")
room.addUser(alice)
room.addUser(bob)
room.addUser(charlie)
alice.send("Hello everyone!") // Broadcast
bob.send("Hi Alice!", "Alice") // Direct message
Example 2: Air Traffic Control
interface ControlTower {
requestLanding(aircraft: Aircraft)
requestTakeoff(aircraft: Aircraft)
notifyRunwayClear(aircraft: Aircraft)
}
class AirportControlTower implements ControlTower {
private queue: List<Aircraft> = []
private runwayFree: Boolean = true
requestLanding(aircraft: Aircraft) {
if runwayFree {
runwayFree = false
aircraft.confirmLanding()
} else {
queue.add(aircraft)
aircraft.holdPosition("Runway occupied. Hold pattern assigned.")
}
}
requestTakeoff(aircraft: Aircraft) {
if runwayFree {
runwayFree = false
aircraft.confirmTakeoff()
} else {
queue.add(aircraft)
aircraft.holdPosition("Runway occupied. Wait for clearance.")
}
}
notifyRunwayClear(aircraft: Aircraft) {
runwayFree = true
if queue.isNotEmpty() {
next = queue.removeFirst()
runwayFree = false
next.confirmLanding() // or confirmTakeoff based on request type
}
}
}
abstract class Aircraft {
protected tower: ControlTower
public callSign: String
land() { tower.requestLanding(this) }
takeoff() { tower.requestTakeoff(this) }
done() { tower.notifyRunwayClear(this) }
abstract confirmLanding()
abstract confirmTakeoff()
abstract holdPosition(reason: String)
}
class CommercialFlight extends Aircraft {
confirmLanding() { print("{callSign}: Landing confirmed. Descending.") }
confirmTakeoff() { print("{callSign}: Takeoff confirmed. Accelerating.") }
holdPosition(r) { print("{callSign}: Holding. Reason: {r}") }
}
// Usage
tower = new AirportControlTower()
flight1 = new CommercialFlight("AA-101", tower)
flight2 = new CommercialFlight("BA-202", tower)
flight1.land() // Runway free → confirmed
flight2.land() // Runway busy → hold
flight1.done() // Runway clear → flight2 gets confirmation
Communication Diagram
Before: Mesh Topology (Without Mediator)
┌───────────────────────────────────┐
│ │
▼ │
┌─────────┐ ┌─────────┐ ┌─────────┐
│ User A │◄────►│ User B │◄────►│ User C │
└─────────┘ └─────────┘ └─────────┘
▲ ▲ ▲
│ │ │
│ ┌─────────┐ │
└──────────►│ User D │◄───────────┘
└─────────┘
Connections: N × (N-1) / 2 = 6 bidirectional links
Adding User E requires up to 4 new connections
After: Star Topology (With Mediator)
┌─────────┐
│ User A │
└────┬─────┘
│
┌─────────┐ │ ┌─────────┐
│ User D │────┼────│ User B │
└─────────┘ │ └─────────┘
│
┌──────┴──────┐
│ MEDIATOR │
│ (ChatRoom) │
└──────┬──────┘
│
┌────┴─────┐
│ User C │
└──────────┘
Connections: N links (one per colleague)
Adding User E requires exactly 1 new connection
Real-World Examples
| Domain | Mediator | Colleagues | Interaction |
|---|---|---|---|
| Chat Applications | Chat room / channel | Users | Messages routed through room; room handles delivery, history, presence |
| Air Traffic Control | Control tower | Aircraft | Tower coordinates runway access, prevents collisions, manages queues |
| GUI Dialog Boxes | Dialog/Form controller | Buttons, text fields, dropdowns, checkboxes | Changing one field enables/disables others, validates combinations |
| Event Buses | Event bus / message broker | Microservices, modules | Publishers emit events; bus routes to registered subscribers |
| Orchestration Services | Workflow orchestrator (e.g., Step Functions) | Lambda functions, services | Orchestrator sequences calls, handles retries, manages state |
| Middleware in Express/Koa | Router/middleware pipeline | Route handlers | Framework mediates request flow through handler chain |
| Redux Store | Store + reducers | React components | Components dispatch actions; store mediates state updates and notifies subscribers |
Mediator vs Observer vs Facade
| Aspect | Mediator | Observer | Facade |
|---|---|---|---|
| Intent | Encapsulate complex interactions between objects | Define one-to-many dependency so dependents are notified of changes | Provide unified interface to a subsystem |
| Direction | Bidirectional - mediator coordinates back and forth | Unidirectional - subject notifies observers | Unidirectional - client calls facade |
| Awareness | Mediator knows all colleagues; colleagues know mediator | Subject doesn't know observer details | Facade knows subsystem; subsystem doesn't know facade |
| Coupling | Colleagues decoupled from each other, coupled to mediator | Observers decoupled from subject's internals | Client decoupled from subsystem complexity |
| Logic location | Coordination logic lives in mediator | No coordination - just notification | No coordination - just delegation |
| Topology | Star (hub-and-spoke) | Fan-out (one-to-many) | Layered (simplification layer) |
| Use case | Complex multi-object coordination | State change broadcasting | Simplifying a complex API |
| Can combine? | Often uses Observer internally for event dispatch | Can be used inside a Mediator | Can wrap a Mediator |
Advantages & Disadvantages
| Advantages | Disadvantages |
|---|---|
| Single Responsibility - interaction logic centralized in one class | God Object risk - mediator can grow into an unmaintainable monolith |
| Open/Closed - new colleagues added without modifying existing ones | Single point of failure - if mediator crashes, all communication stops |
| Reduced coupling - colleagues are independent and reusable | Performance bottleneck - all messages funnel through one object |
| Easier debugging - trace all interactions through one point | Indirection overhead - simple interactions become unnecessarily complex |
| Replaceable coordination - swap mediator for different behaviour | Testing complexity - mediator itself may need extensive testing |
| Promotes composition - colleagues composed via mediator, not inheritance | Hidden dependencies - interactions not visible in colleague interfaces |
| Simplifies object protocols - colleagues have simpler interfaces | Harder to understand flow - must read mediator to understand system behaviour |
Constraints & Edge Cases
God Mediator Anti-Pattern
The most common pitfall. As the system grows, developers keep adding coordination logic to the mediator until it becomes a 5000-line class that knows everything about every colleague. Mitigation: Split into multiple focused mediators by domain, or use event-based mediation where handlers are separate classes.
Mediator Becoming Too Complex
When the mediator has more conditional logic than all colleagues combined, the pattern is being misapplied. Consider whether some logic belongs back in the colleagues or whether you need a chain of mediators with clear responsibilities.
Performance Bottleneck
In high-throughput systems, routing every message through a single mediator creates contention. Mitigations:
- Use async/non-blocking mediation
- Partition mediators by topic or domain
- Use lock-free data structures for the message queue
- Consider direct communication for latency-critical paths
Testing Mediator Logic
The mediator accumulates complex conditional logic. Test strategies:
- Unit test the mediator with mock colleagues
- Test each coordination scenario as an integration test
- Use state machines to model and verify mediator transitions
- Property-based testing for invariants (e.g., "only one aircraft on runway at a time")
Circular Notification Loops
Colleague A notifies mediator → mediator updates Colleague B → B notifies mediator → mediator updates A → infinite loop. Prevention: Use flags to suppress re-entrant notifications, or design events as one-shot (no cascading).
Interview Follow-ups
Q1: How does the Mediator pattern differ from a simple Event Bus?
A: An event bus is a generic publish-subscribe infrastructure - it routes messages but contains no domain logic. A mediator encapsulates specific coordination rules. The event bus says "deliver this event to whoever subscribed." The mediator says "when User A sends a message and User B is offline, queue it and notify User B's mobile device." The mediator contains business logic; the event bus is just plumbing. In practice, a mediator often uses an event bus internally.
Q2: Can you have multiple mediators in one system?
A: Absolutely, and you should when the system is large. Each mediator handles a bounded context of interactions. For example, a GUI application might have one mediator per dialog/form, not one global mediator for the entire UI. In microservices, each bounded context might have its own orchestrator. The key is that within a given interaction group, colleagues communicate through their specific mediator.
Q3: How would you refactor an existing tightly-coupled system to use the Mediator pattern?
A: Step-by-step: (1) Identify the cluster of objects with heavy cross-references. (2) Extract the interaction logic from these objects into a new mediator class. (3) Replace direct references between colleagues with a reference to the mediator. (4) Have colleagues notify the mediator of state changes instead of calling peers directly. (5) Move conditional coordination logic from colleagues into the mediator's notify/handle methods. (6) Verify that no colleague imports or references another colleague.
Q4: How does CQRS relate to the Mediator pattern?
Hint: Think about how commands and queries are dispatched to their respective handlers without the caller knowing which handler processes them. Consider the role of the mediator as a dispatcher in the CQRS pipeline.
Q5: What happens when the mediator needs to be distributed across multiple nodes?
Hint: Consider how message brokers (Kafka, RabbitMQ) act as distributed mediators. Think about consistency guarantees, ordering, and what happens during network partitions. How does the CAP theorem constrain a distributed mediator?
Counter Questions to Ask Interviewer
-
"How many objects are involved in the interaction, and how frequently do their communication patterns change?" - Determines if the pattern's overhead is justified.
-
"Is the coordination logic purely routing, or does it contain business rules?" - Distinguishes whether you need a mediator (business logic) or a simple event bus (routing only).
-
"Are there performance or latency constraints that would make centralized mediation a bottleneck?" - Identifies if you need partitioned mediators or direct communication for hot paths.
-
"Should the mediator be synchronous or asynchronous? Are there ordering guarantees required?" - Shapes the implementation approach significantly.
-
"Is this a single-process system or distributed? Do colleagues live in different services?" - Determines if you need an in-process mediator or a distributed message broker acting as mediator.
References & Whitepapers
-
Gamma, E., Helm, R., Johnson, R., Vlissides, J. - Design Patterns: Elements of Reusable Object-Oriented Software (GoF, 1994), Chapter 5: Behavioral Patterns - Mediator.
-
MediatR Library - .NET implementation of the mediator pattern for in-process messaging. Supports request/response, commands, queries, notifications, and pipeline behaviours. github.com/jbogard/MediatR
-
CQRS with Mediator - Jimmy Bogard, "CQRS and MediatR" - demonstrates how the mediator pattern enables clean separation of command and query handlers without direct coupling between controllers and business logic.
-
Enterprise Integration Patterns - Hohpe, G., Woolf, B. (2003) - Message Broker and Message Router patterns are distributed manifestations of the Mediator pattern.
-
Martin Fowler - "Mediator" in Patterns of Enterprise Application Architecture - discusses mediator in the context of domain event handling and workflow coordination.
-
Microsoft Documentation - "Implement the microservice application layer using the Mediator pattern" - practical guidance on using MediatR in ASP.NET Core CQRS architectures.
Related Topics
- Observer Pattern - Often used internally by mediators for event dispatch
- Command Pattern - Commands routed through mediator in CQRS
- Chain of Responsibility - Alternative for sequential processing vs centralized coordination
- Facade Pattern - Simplifies interface but doesn't coordinate bidirectional interaction
- State Pattern - Mediator can use state machines to manage complex coordination protocols
- Event-Driven Architecture - Distributed mediation at system scale