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 UseWhen NOT to Use
Multiple objects communicate in complex, hard-to-understand waysOnly 2-3 objects interact with simple, stable relationships
Reusing an object is difficult because it refers to many othersCommunication patterns are unlikely to change
You want to customize interaction behaviour without subclassing many classesThe "mediation" logic is trivial (just forwarding calls)
Coordination logic changes frequently and should be centralizedPerformance is critical and the indirection overhead matters
You need to decouple a set of classes that were designed to work togetherObjects genuinely need direct references for tight real-time coupling
Event-driven architectures where producers shouldn't know consumersThe mediator would become a god object with thousands of lines
GUI form coordination where field changes cascade to other fieldsSimple 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:

  1. Colleague emits a typed event/message to the mediator
  2. Mediator has registered handlers for each event type
  3. Handlers execute the coordination logic
  4. 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

DomainMediatorColleaguesInteraction
Chat ApplicationsChat room / channelUsersMessages routed through room; room handles delivery, history, presence
Air Traffic ControlControl towerAircraftTower coordinates runway access, prevents collisions, manages queues
GUI Dialog BoxesDialog/Form controllerButtons, text fields, dropdowns, checkboxesChanging one field enables/disables others, validates combinations
Event BusesEvent bus / message brokerMicroservices, modulesPublishers emit events; bus routes to registered subscribers
Orchestration ServicesWorkflow orchestrator (e.g., Step Functions)Lambda functions, servicesOrchestrator sequences calls, handles retries, manages state
Middleware in Express/KoaRouter/middleware pipelineRoute handlersFramework mediates request flow through handler chain
Redux StoreStore + reducersReact componentsComponents dispatch actions; store mediates state updates and notifies subscribers

Mediator vs Observer vs Facade

AspectMediatorObserverFacade
IntentEncapsulate complex interactions between objectsDefine one-to-many dependency so dependents are notified of changesProvide unified interface to a subsystem
DirectionBidirectional - mediator coordinates back and forthUnidirectional - subject notifies observersUnidirectional - client calls facade
AwarenessMediator knows all colleagues; colleagues know mediatorSubject doesn't know observer detailsFacade knows subsystem; subsystem doesn't know facade
CouplingColleagues decoupled from each other, coupled to mediatorObservers decoupled from subject's internalsClient decoupled from subsystem complexity
Logic locationCoordination logic lives in mediatorNo coordination - just notificationNo coordination - just delegation
TopologyStar (hub-and-spoke)Fan-out (one-to-many)Layered (simplification layer)
Use caseComplex multi-object coordinationState change broadcastingSimplifying a complex API
Can combine?Often uses Observer internally for event dispatchCan be used inside a MediatorCan wrap a Mediator

Advantages & Disadvantages

AdvantagesDisadvantages
Single Responsibility - interaction logic centralized in one classGod Object risk - mediator can grow into an unmaintainable monolith
Open/Closed - new colleagues added without modifying existing onesSingle point of failure - if mediator crashes, all communication stops
Reduced coupling - colleagues are independent and reusablePerformance bottleneck - all messages funnel through one object
Easier debugging - trace all interactions through one pointIndirection overhead - simple interactions become unnecessarily complex
Replaceable coordination - swap mediator for different behaviourTesting complexity - mediator itself may need extensive testing
Promotes composition - colleagues composed via mediator, not inheritanceHidden dependencies - interactions not visible in colleague interfaces
Simplifies object protocols - colleagues have simpler interfacesHarder 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

  1. "How many objects are involved in the interaction, and how frequently do their communication patterns change?" - Determines if the pattern's overhead is justified.

  2. "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).

  3. "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.

  4. "Should the mediator be synchronous or asynchronous? Are there ordering guarantees required?" - Shapes the implementation approach significantly.

  5. "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

  1. Gamma, E., Helm, R., Johnson, R., Vlissides, J. - Design Patterns: Elements of Reusable Object-Oriented Software (GoF, 1994), Chapter 5: Behavioral Patterns - Mediator.

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

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

  4. Enterprise Integration Patterns - Hohpe, G., Woolf, B. (2003) - Message Broker and Message Router patterns are distributed manifestations of the Mediator pattern.

  5. Martin Fowler - "Mediator" in Patterns of Enterprise Application Architecture - discusses mediator in the context of domain event handling and workflow coordination.

  6. Microsoft Documentation - "Implement the microservice application layer using the Mediator pattern" - practical guidance on using MediatR in ASP.NET Core CQRS architectures.