Skip to main content

Mediator Pattern 🎭

Definition: The Mediator pattern defines how a set of objects interact with each other. It promotes loose coupling by keeping objects from referring to each other explicitly, and lets you vary their interaction independently.

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

🤔 Problem

When objects in a system communicate directly with each other, they become tightly coupled:

  • Complex Dependencies: Objects have many direct references to other objects
  • Difficult Maintenance: Changes to one object affect many others
  • Reusability Issues: Objects are hard to reuse in different contexts
  • Communication Chaos: Many-to-many relationships create complexity

Think of an air traffic control system - without a central controller, pilots would need to communicate directly with every other plane, creating chaos.

💡 Solution

The Mediator pattern suggests creating a mediator object that handles all communications between objects. Objects don't communicate directly but through the mediator, which coordinates their interactions.

🏗️ Structure

Mediator (interface)
└── +mediate(sender: Component, event: string): void

ConcreteMediator implements Mediator
├── -componentA: ComponentA
├── -componentB: ComponentB
├── +mediate(sender: Component, event: string): void

Component (abstract)
├── +mediator: Mediator
├── +notify(event: string): void

ComponentA, ComponentB extend Component
├── +doA(), +doB() → notify("eventA"), notify("eventB")

💻 Simple Example

Chat Room Mediator

// Mediator interface
class ChatMediator {
sendMessage(message, user) {
throw new Error("sendMessage() method must be implemented");
}

addUser(user) {
throw new Error("addUser() method must be implemented");
}

removeUser(user) {
throw new Error("removeUser() method must be implemented");
}
}

// Concrete Mediator - Chat Room
class ChatRoom extends ChatMediator {
constructor(name) {
super();
this.name = name;
this.users = [];
this.messageHistory = [];
this.created = new Date();
}

addUser(user) {
this.users.push(user);
user.setChatRoom(this);
console.log(`👥 ${user.name} joined the chat room: ${this.name}`);

// Notify other users
this.broadcastSystemMessage(`${user.name} joined the chat`);

// Show recent messages to new user
if (this.messageHistory.length > 0) {
console.log(`📜 Showing recent messages to ${user.name}:`);
this.messageHistory.slice(-3).forEach(msg => {
console.log(` ${msg.timestamp.toLocaleTimeString()} ${msg.sender}: ${msg.text}`);
});
}
}

removeUser(user) {
const index = this.users.indexOf(user);
if (index !== -1) {
this.users.splice(index, 1);
console.log(`👋 ${user.name} left the chat room: ${this.name}`);

// Notify other users
this.broadcastSystemMessage(`${user.name} left the chat`);
}
}

sendMessage(message, sender) {
console.log(`💬 [${this.name}] ${sender.name}: ${message}`);

// Store message in history
this.messageHistory.push({
sender: sender.name,
text: message,
timestamp: new Date()
});

// Deliver to all other users
this.users.forEach(user => {
if (user !== sender) {
user.receiveMessage(message, sender.name);
}
});
}

broadcastSystemMessage(message) {
console.log(`🔔 [${this.name}] System: ${message}`);

this.users.forEach(user => {
user.receiveSystemMessage(message);
});
}

getUserCount() {
return this.users.length;
}

getStats() {
console.log(`📊 Chat Room "${this.name}" Stats:`);
console.log(` Active Users: ${this.users.length}`);
console.log(` Total Messages: ${this.messageHistory.length}`);
console.log(` Created: ${this.created.toLocaleString()}`);
if (this.users.length > 0) {
console.log(` Users: ${this.users.map(u => u.name).join(', ')}`);
}
}
}

// Abstract User component
class ChatUser {
constructor(name) {
this.name = name;
this.chatRoom = null;
this.messagesSent = 0;
this.messagesReceived = 0;
}

setChatRoom(chatRoom) {
this.chatRoom = chatRoom;
}

sendMessage(message) {
if (!this.chatRoom) {
console.log(`${this.name}: Not connected to any chat room`);
return;
}

this.messagesSent++;
this.chatRoom.sendMessage(message, this);
}

receiveMessage(message, senderName) {
this.messagesReceived++;
console.log(`📱 ${this.name} received: "${message}" from ${senderName}`);
}

receiveSystemMessage(message) {
console.log(`🔔 ${this.name} received system message: ${message}`);
}

leaveChatRoom() {
if (this.chatRoom) {
this.chatRoom.removeUser(this);
this.chatRoom = null;
}
}

getStats() {
console.log(`📈 ${this.name}'s Stats:`);
console.log(` Messages Sent: ${this.messagesSent}`);
console.log(` Messages Received: ${this.messagesReceived}`);
console.log(` Current Room: ${this.chatRoom ? this.chatRoom.name : 'None'}`);
}
}

// Usage
console.log("=== Chat Room Mediator Demo ===\n");

console.log("Creating chat room and users:");
console.log("-".repeat(30));

// Create chat room (mediator)
const generalChat = new ChatRoom("General Chat");

// Create users (components)
const alice = new ChatUser("Alice");
const bob = new ChatUser("Bob");
const charlie = new ChatUser("Charlie");

console.log("\nUsers joining chat room:");
console.log("-".repeat(25));

// Users join chat room
generalChat.addUser(alice);
generalChat.addUser(bob);
generalChat.addUser(charlie);

console.log("\n" + "=".repeat(50) + "\n");

console.log("Chat conversation:");
console.log("-".repeat(17));

// Users send messages through the mediator
alice.sendMessage("Hello everyone! 👋");
bob.sendMessage("Hi Alice! How are you?");
charlie.sendMessage("Good morning! ☀️");
alice.sendMessage("I'm doing great, thanks for asking!");
bob.sendMessage("Anyone up for lunch later?");
charlie.sendMessage("Count me in! 🍕");

console.log("\n" + "=".repeat(50) + "\n");

console.log("User leaving chat:");
console.log("-".repeat(18));

bob.leaveChatRoom();

console.log("\nContinuing conversation:");
console.log("-".repeat(25));

alice.sendMessage("Bob left us 😢");
charlie.sendMessage("We'll catch up with him later");

console.log("\n" + "=".repeat(50) + "\n");

console.log("Statistics:");
console.log("-".repeat(11));

generalChat.getStats();
console.log();
alice.getStats();
console.log();
charlie.getStats();

🌟 Real-World Example

Air Traffic Control System

// Mediator interface
class AirTrafficControl {
requestLanding(aircraft) {
throw new Error("requestLanding() method must be implemented");
}

requestTakeoff(aircraft) {
throw new Error("requestTakeoff() method must be implemented");
}

registerAircraft(aircraft) {
throw new Error("registerAircraft() method must be implemented");
}
}

// Concrete Mediator - Airport Control Tower
class ControlTower extends AirTrafficControl {
constructor(airportCode) {
super();
this.airportCode = airportCode;
this.aircrafts = [];
this.runwayStatus = {
runway1: 'free', // 'free', 'occupied', 'maintenance'
runway2: 'free'
};
this.landingQueue = [];
this.takeoffQueue = [];
this.weatherConditions = 'clear'; // 'clear', 'storm', 'fog'
}

registerAircraft(aircraft) {
this.aircrafts.push(aircraft);
aircraft.setControlTower(this);
console.log(`✈️ Aircraft ${aircraft.callSign} registered with ${this.airportCode} Control Tower`);

// Send weather update
this.sendWeatherUpdate(aircraft);
}

requestLanding(aircraft) {
console.log(`📡 ${this.airportCode} Tower: Landing request from ${aircraft.callSign}`);

// Check weather conditions
if (this.weatherConditions !== 'clear') {
console.log(`${this.airportCode} Tower: Landing denied for ${aircraft.callSign} - Weather: ${this.weatherConditions}`);
aircraft.receiveMessage(`Landing denied - weather conditions: ${this.weatherConditions}`);
return false;
}

// Check runway availability
const availableRunway = this.getAvailableRunway();
if (availableRunway) {
this.assignRunwayForLanding(aircraft, availableRunway);
return true;
} else {
// Add to queue
this.landingQueue.push(aircraft);
console.log(`${this.airportCode} Tower: ${aircraft.callSign} added to landing queue (position ${this.landingQueue.length})`);
aircraft.receiveMessage(`Added to landing queue, position ${this.landingQueue.length}`);
return false;
}
}

requestTakeoff(aircraft) {
console.log(`📡 ${this.airportCode} Tower: Takeoff request from ${aircraft.callSign}`);

// Check weather conditions
if (this.weatherConditions !== 'clear') {
console.log(`${this.airportCode} Tower: Takeoff denied for ${aircraft.callSign} - Weather: ${this.weatherConditions}`);
aircraft.receiveMessage(`Takeoff denied - weather conditions: ${this.weatherConditions}`);
return false;
}

// Check runway availability
const availableRunway = this.getAvailableRunway();
if (availableRunway) {
this.assignRunwayForTakeoff(aircraft, availableRunway);
return true;
} else {
// Add to queue
this.takeoffQueue.push(aircraft);
console.log(`${this.airportCode} Tower: ${aircraft.callSign} added to takeoff queue (position ${this.takeoffQueue.length})`);
aircraft.receiveMessage(`Added to takeoff queue, position ${this.takeoffQueue.length}`);
return false;
}
}

getAvailableRunway() {
for (const [runway, status] of Object.entries(this.runwayStatus)) {
if (status === 'free') {
return runway;
}
}
return null;
}

assignRunwayForLanding(aircraft, runway) {
this.runwayStatus[runway] = 'occupied';
console.log(`🛬 ${this.airportCode} Tower: ${aircraft.callSign} cleared for landing on ${runway}`);
aircraft.receiveMessage(`Cleared for landing on ${runway}`);

// Simulate landing time
setTimeout(() => {
this.completeLanding(aircraft, runway);
}, 2000);
}

assignRunwayForTakeoff(aircraft, runway) {
this.runwayStatus[runway] = 'occupied';
console.log(`🛫 ${this.airportCode} Tower: ${aircraft.callSign} cleared for takeoff on ${runway}`);
aircraft.receiveMessage(`Cleared for takeoff on ${runway}`);

// Simulate takeoff time
setTimeout(() => {
this.completeTakeoff(aircraft, runway);
}, 1500);
}

completeLanding(aircraft, runway) {
this.runwayStatus[runway] = 'free';
console.log(`${this.airportCode} Tower: ${aircraft.callSign} landed successfully on ${runway}`);
aircraft.receiveMessage(`Landing complete on ${runway}. Taxi to gate.`);

// Process next in queue
this.processQueues();
}

completeTakeoff(aircraft, runway) {
this.runwayStatus[runway] = 'free';
console.log(`${this.airportCode} Tower: ${aircraft.callSign} departed successfully from ${runway}`);
aircraft.receiveMessage(`Takeoff complete. Contact departure control.`);

// Process next in queue
this.processQueues();
}

processQueues() {
// Process landing queue first (priority)
if (this.landingQueue.length > 0) {
const nextAircraft = this.landingQueue.shift();
this.requestLanding(nextAircraft);
} else if (this.takeoffQueue.length > 0) {
const nextAircraft = this.takeoffQueue.shift();
this.requestTakeoff(nextAircraft);
}
}

sendWeatherUpdate(aircraft) {
aircraft.receiveMessage(`Current weather: ${this.weatherConditions}`);
}

setWeatherConditions(conditions) {
const oldConditions = this.weatherConditions;
this.weatherConditions = conditions;
console.log(`🌤️ ${this.airportCode} Tower: Weather changed from ${oldConditions} to ${conditions}`);

// Broadcast to all aircraft
this.aircrafts.forEach(aircraft => {
aircraft.receiveMessage(`Weather update: ${conditions}`);
});
}

getStatus() {
console.log(`📊 ${this.airportCode} Control Tower Status:`);
console.log(` Weather: ${this.weatherConditions}`);
console.log(` Runway 1: ${this.runwayStatus.runway1}`);
console.log(` Runway 2: ${this.runwayStatus.runway2}`);
console.log(` Landing Queue: ${this.landingQueue.length} aircraft`);
console.log(` Takeoff Queue: ${this.takeoffQueue.length} aircraft`);
console.log(` Registered Aircraft: ${this.aircrafts.length}`);
}
}

// Abstract Aircraft component
class Aircraft {
constructor(callSign, type, airline) {
this.callSign = callSign;
this.type = type;
this.airline = airline;
this.controlTower = null;
this.status = 'in-flight'; // 'in-flight', 'landed', 'taxiing', 'departed'
this.messages = [];
}

setControlTower(controlTower) {
this.controlTower = controlTower;
}

requestLanding() {
if (!this.controlTower) {
console.log(`${this.callSign}: No control tower assigned`);
return;
}

console.log(`📞 ${this.callSign}: Requesting landing permission`);
return this.controlTower.requestLanding(this);
}

requestTakeoff() {
if (!this.controlTower) {
console.log(`${this.callSign}: No control tower assigned`);
return;
}

console.log(`📞 ${this.callSign}: Requesting takeoff permission`);
return this.controlTower.requestTakeoff(this);
}

receiveMessage(message) {
this.messages.push({
message,
timestamp: new Date()
});
console.log(`📻 ${this.callSign} received: "${message}"`);
}

getInfo() {
console.log(`✈️ Aircraft Info:`);
console.log(` Call Sign: ${this.callSign}`);
console.log(` Type: ${this.type}`);
console.log(` Airline: ${this.airline}`);
console.log(` Status: ${this.status}`);
console.log(` Messages Received: ${this.messages.length}`);
}
}

// Usage
console.log("\n=== Air Traffic Control Mediator Demo ===\n");

console.log("Setting up airport control tower:");
console.log("-".repeat(35));

const jfkTower = new ControlTower("JFK");

console.log("Aircraft requesting services:");
console.log("-".repeat(30));

// Create aircraft
const flight123 = new Aircraft("AA123", "Boeing 737", "American Airlines");
const flight456 = new Aircraft("UA456", "Airbus A320", "United Airlines");
const flight789 = new Aircraft("DL789", "Boeing 777", "Delta Airlines");

// Register aircraft with control tower
jfkTower.registerAircraft(flight123);
jfkTower.registerAircraft(flight456);
jfkTower.registerAircraft(flight789);

console.log("\n" + "=".repeat(50) + "\n");

console.log("Flight operations:");
console.log("-".repeat(18));

// Multiple aircraft requesting landing
flight123.requestLanding();
flight456.requestLanding();
flight789.requestLanding();

console.log("\n" + "-".repeat(30) + "\n");

// Wait a bit and check status
setTimeout(() => {
console.log("\nCurrent airport status:");
console.log("-".repeat(23));
jfkTower.getStatus();

console.log("\nWeather change:");
console.log("-".repeat(15));
jfkTower.setWeatherConditions('fog');

// New aircraft trying to land in bad weather
const emergencyFlight = new Aircraft("EM911", "Boeing 767", "Emergency Medical");
jfkTower.registerAircraft(emergencyFlight);
emergencyFlight.requestLanding();

}, 3000);

✅ Pros

  • Loose Coupling: Components don't reference each other directly
  • Centralized Control: Complex interactions are managed in one place
  • Reusability: Components can be reused in different mediator contexts
  • Easy Maintenance: Changes to interaction logic are localized
  • Single Responsibility: Each component focuses on its core functionality

❌ Cons

  • God Object: Mediator can become overly complex
  • Performance: Additional layer of indirection
  • Single Point of Failure: If mediator fails, all communication fails
  • Complexity Transfer: Moves complexity from components to mediator

🎯 When to Use

  • Complex Communication: Many objects communicate in complex ways
  • Tight Coupling: Objects are tightly coupled through communication
  • Centralized Logic: Need to centralize complex interaction logic
  • Reusable Components: Want to make components reusable across contexts
  • Communication Protocol: Need to change communication protocol dynamically

🔄 Mediator Variations

1. Observer-based Mediator

class ObserverMediator extends EventEmitter {
constructor() {
super();
this.components = [];
}

register(component) {
this.components.push(component);
component.setMediator(this);
}

mediate(event, data, sender) {
this.emit(event, data, sender);
}
}

2. Command-based Mediator

class CommandMediator {
constructor() {
this.commandHandlers = new Map();
}

registerHandler(command, handler) {
this.commandHandlers.set(command, handler);
}

execute(command, data) {
const handler = this.commandHandlers.get(command);
if (handler) {
return handler(data);
}
}
}

3. State-based Mediator

class StatefulMediator {
constructor() {
this.state = {};
this.components = [];
}

updateState(key, value) {
this.state[key] = value;
this.notifyComponents(key, value);
}

notifyComponents(key, value) {
this.components.forEach(component => {
component.onStateChange(key, value);
});
}
}
  • Observer: Mediator can use Observer pattern for event broadcasting
  • Command: Commands can be used to encapsulate requests sent through mediator
  • Facade: Both provide simplified interface, but Mediator coordinates while Facade simplifies
  • Chain of Responsibility: Both handle requests, but Chain passes requests while Mediator coordinates

📚 Further Reading