Mediator Pattern: Reduce Object Coupling
TL;DR
The Mediator Pattern centralizes complex communication between objects by introducing a mediator object that handles all interactions. Instead of objects referencing each other directly (creating tight coupling), they communicate through the mediator, reducing dependencies and making the system easier to maintain and extend.
Core Concept
What is the Mediator Pattern?
The Mediator Pattern is a behavioral design pattern that reduces chaotic dependencies between objects by making them communicate indirectly through a mediator object. Think of it like an air traffic control tower: planes don’t talk directly to each other (which would be dangerous and chaotic), they all communicate through the tower.
The Problem It Solves
When objects need to interact with multiple other objects, direct communication creates a web of dependencies. Each object needs references to many others, making the code:
- Tightly coupled: Changes to one object affect many others
- Hard to maintain: Adding new objects requires updating existing ones
- Difficult to reuse: Objects can’t be used independently
- Complex to understand: The interaction logic is scattered everywhere
How It Works
The pattern introduces a mediator object that encapsulates how objects interact. Components (called colleagues) only know about the mediator, not each other. When a colleague needs to communicate, it tells the mediator, which then coordinates with other colleagues.
Key participants:
- Mediator: Defines the interface for communicating with colleagues
- ConcreteMediator: Implements coordination logic and maintains references to colleagues
- Colleague: Components that communicate through the mediator
When to Use It
- Objects communicate in complex but well-defined ways
- Reusing objects is difficult due to many dependencies
- Behavior distributed among classes should be customizable without subclassing
- You want to centralize control logic in one place
Real-World Analogies
- Chat room: Users send messages to the room, which distributes them
- Airport control tower: Coordinates all aircraft without them talking directly
- GUI dialog boxes: Form fields notify a dialog manager of changes
- Stock exchange: Buyers and sellers trade through the exchange, not directly
Visual Guide
Direct Communication (Without Mediator)
graph TD
A[Component A] <--> B[Component B]
A <--> C[Component C]
A <--> D[Component D]
B <--> C
B <--> D
C <--> D
style A fill:#ff6b6b
style B fill:#ff6b6b
style C fill:#ff6b6b
style D fill:#ff6b6b
Without a mediator, components have many direct dependencies (n(n-1)/2 connections for n components). This creates tight coupling and makes changes difficult.*
Mediator Pattern Structure
graph TD
M[Mediator]
A[Component A] --> M
B[Component B] --> M
C[Component C] --> M
D[Component D] --> M
M -.notifies.-> A
M -.notifies.-> B
M -.notifies.-> C
M -.notifies.-> D
style M fill:#4ecdc4
style A fill:#95e1d3
style B fill:#95e1d3
style C fill:#95e1d3
style D fill:#95e1d3
With a mediator, components only depend on the mediator (n connections). The mediator coordinates all interactions, reducing coupling.
Chat Room Example Flow
sequenceDiagram
participant U1 as User 1
participant CR as ChatRoom
participant U2 as User 2
participant U3 as User 3
U1->>CR: send("Hello!")
CR->>U2: receive("User 1: Hello!")
CR->>U3: receive("User 1: Hello!")
Note over CR: Mediator handles<br/>message distribution
Sequence diagram showing how the mediator (ChatRoom) coordinates communication between users without them knowing about each other.
Examples
Example 1: Chat Room System
Let’s build a chat room where users can send messages without knowing about other users.
from abc import ABC, abstractmethod
from typing import List
# Mediator interface
class ChatMediator(ABC):
@abstractmethod
def send_message(self, message: str, sender: 'User') -> None:
pass
@abstractmethod
def add_user(self, user: 'User') -> None:
pass
# Concrete Mediator
class ChatRoom(ChatMediator):
def __init__(self, name: str):
self.name = name
self._users: List[User] = []
def add_user(self, user: 'User') -> None:
self._users.append(user)
print(f"{user.name} joined {self.name}")
def send_message(self, message: str, sender: 'User') -> None:
# Mediator coordinates: send to all except sender
for user in self._users:
if user != sender:
user.receive(f"{sender.name}: {message}")
# Colleague
class User:
def __init__(self, name: str, mediator: ChatMediator):
self.name = name
self._mediator = mediator
self._mediator.add_user(self)
def send(self, message: str) -> None:
print(f"{self.name} sends: {message}")
self._mediator.send_message(message, self)
def receive(self, message: str) -> None:
print(f"{self.name} receives: {message}")
# Usage
chat_room = ChatRoom("Python Developers")
alice = User("Alice", chat_room)
bob = User("Bob", chat_room)
charlie = User("Charlie", chat_room)
alice.send("Hey everyone!")
bob.send("Hi Alice!")
Expected Output:
Alice joined Python Developers
Bob joined Python Developers
Charlie joined Python Developers
Alice sends: Hey everyone!
Bob receives: Alice: Hey everyone!
Charlie receives: Alice: Hey everyone!
Bob sends: Hi Alice!
Alice receives: Bob: Hi Alice!
Charlie receives: Bob: Hi Alice!
Key Points:
- Users don’t hold references to other users
- ChatRoom mediator handles all message routing
- Adding new users doesn’t require changing existing user code
- Communication logic is centralized in one place
Try it yourself: Extend the chat room to support private messages between two specific users.
Example 2: Air Traffic Control System
A more complex example showing how the mediator coordinates aircraft landing and takeoff.
from abc import ABC, abstractmethod
from enum import Enum
from typing import List, Optional
class FlightStatus(Enum):
WAITING = "waiting"
CLEARED = "cleared"
LANDED = "landed"
# Mediator interface
class ATCMediator(ABC):
@abstractmethod
def request_landing(self, aircraft: 'Aircraft') -> bool:
pass
@abstractmethod
def request_takeoff(self, aircraft: 'Aircraft') -> bool:
pass
@abstractmethod
def notify_landed(self, aircraft: 'Aircraft') -> None:
pass
# Concrete Mediator
class ControlTower(ATCMediator):
def __init__(self, name: str):
self.name = name
self._runway_available = True
self._waiting_queue: List[Aircraft] = []
def request_landing(self, aircraft: 'Aircraft') -> bool:
print(f"[{self.name}] {aircraft.call_sign} requests landing")
if self._runway_available:
self._runway_available = False
print(f"[{self.name}] Cleared to land: {aircraft.call_sign}")
return True
else:
print(f"[{self.name}] Runway occupied. {aircraft.call_sign} hold position")
self._waiting_queue.append(aircraft)
return False
def request_takeoff(self, aircraft: 'Aircraft') -> bool:
print(f"[{self.name}] {aircraft.call_sign} requests takeoff")
if self._runway_available:
self._runway_available = False
print(f"[{self.name}] Cleared for takeoff: {aircraft.call_sign}")
return True
else:
print(f"[{self.name}] Runway occupied. {aircraft.call_sign} wait")
return False
def notify_landed(self, aircraft: 'Aircraft') -> None:
print(f"[{self.name}] {aircraft.call_sign} has landed")
self._runway_available = True
# Process waiting queue
if self._waiting_queue:
next_aircraft = self._waiting_queue.pop(0)
print(f"[{self.name}] Clearing next aircraft: {next_aircraft.call_sign}")
next_aircraft.receive_clearance()
# Colleague
class Aircraft:
def __init__(self, call_sign: str, atc: ATCMediator):
self.call_sign = call_sign
self._atc = atc
self.status = FlightStatus.WAITING
def request_landing(self) -> None:
cleared = self._atc.request_landing(self)
if cleared:
self.status = FlightStatus.CLEARED
self.land()
def land(self) -> None:
print(f"[{self.call_sign}] Landing...")
self.status = FlightStatus.LANDED
self._atc.notify_landed(self)
def receive_clearance(self) -> None:
print(f"[{self.call_sign}] Received clearance")
self.status = FlightStatus.CLEARED
self.land()
def request_takeoff(self) -> None:
cleared = self._atc.request_takeoff(self)
if cleared:
self.takeoff()
def takeoff(self) -> None:
print(f"[{self.call_sign}] Taking off...")
self._atc.notify_landed(self) # Runway becomes available
# Usage
tower = ControlTower("JFK Tower")
flight1 = Aircraft("AA101", tower)
flight2 = Aircraft("UA202", tower)
flight3 = Aircraft("DL303", tower)
print("=== Scenario: Multiple landing requests ===")
flight1.request_landing()
flight2.request_landing() # Will be queued
flight3.request_landing() # Will be queued
Expected Output:
=== Scenario: Multiple landing requests ===
[JFK Tower] AA101 requests landing
[JFK Tower] Cleared to land: AA101
[AA101] Landing...
[JFK Tower] AA101 has landed
[JFK Tower] Clearing next aircraft: UA202
[UA202] Received clearance
[UA202] Landing...
[JFK Tower] UA202 has landed
[JFK Tower] Clearing next aircraft: DL303
[DL303] Received clearance
[DL303] Landing...
[JFK Tower] DL303 has landed
[JFK Tower] UA202 requests landing
[JFK Tower] Runway occupied. UA202 hold position
[JFK Tower] DL303 requests landing
[JFK Tower] Runway occupied. DL303 hold position
Key Points:
- Aircraft don’t know about each other’s existence
- ControlTower manages runway availability and queuing
- Complex coordination logic is centralized
- Easy to add features like priority handling or multiple runways
Java/C++ Notes:
- In Java, use interfaces for the mediator and abstract classes for colleagues
- C++ would use pure virtual functions and smart pointers for memory management
- Both languages benefit from explicit interface definitions
Try it yourself: Add a priority system where emergency aircraft get immediate clearance, bumping others in the queue.
Example 3: GUI Form Validation
A practical example showing form fields coordinating through a mediator.
from abc import ABC, abstractmethod
from typing import Dict, Optional
# Mediator interface
class FormMediator(ABC):
@abstractmethod
def notify(self, sender: 'FormField', event: str) -> None:
pass
# Concrete Mediator
class RegistrationForm(FormMediator):
def __init__(self):
self.username: Optional['TextField'] = None
self.email: Optional['TextField'] = None
self.password: Optional['TextField'] = None
self.confirm_password: Optional['TextField'] = None
self.submit_button: Optional['Button'] = None
def notify(self, sender: 'FormField', event: str) -> None:
if event == "value_changed":
self._validate_form()
elif event == "submit":
self._submit_form()
def _validate_form(self) -> None:
# Centralized validation logic
is_valid = True
errors = []
if self.username and len(self.username.value) < 3:
is_valid = False
errors.append("Username must be at least 3 characters")
if self.email and '@' not in self.email.value:
is_valid = False
errors.append("Invalid email format")
if self.password and len(self.password.value) < 8:
is_valid = False
errors.append("Password must be at least 8 characters")
if (self.password and self.confirm_password and
self.password.value != self.confirm_password.value):
is_valid = False
errors.append("Passwords don't match")
# Update submit button state
if self.submit_button:
self.submit_button.set_enabled(is_valid)
if errors:
print(f"Validation errors: {', '.join(errors)}")
else:
print("Form is valid")
def _submit_form(self) -> None:
print(f"\n=== Submitting Registration ===")
print(f"Username: {self.username.value}")
print(f"Email: {self.email.value}")
print(f"Registration successful!")
# Colleague base class
class FormField(ABC):
def __init__(self, name: str, mediator: FormMediator):
self.name = name
self._mediator = mediator
self.value = ""
# Concrete Colleagues
class TextField(FormField):
def set_value(self, value: str) -> None:
self.value = value
print(f"{self.name} changed to: {value}")
self._mediator.notify(self, "value_changed")
class Button(FormField):
def __init__(self, name: str, mediator: FormMediator):
super().__init__(name, mediator)
self.enabled = False
def set_enabled(self, enabled: bool) -> None:
self.enabled = enabled
status = "enabled" if enabled else "disabled"
print(f"{self.name} is now {status}")
def click(self) -> None:
if self.enabled:
print(f"{self.name} clicked")
self._mediator.notify(self, "submit")
else:
print(f"{self.name} is disabled, cannot submit")
# Usage
form = RegistrationForm()
# Create and register fields
form.username = TextField("Username", form)
form.email = TextField("Email", form)
form.password = TextField("Password", form)
form.confirm_password = TextField("Confirm Password", form)
form.submit_button = Button("Submit", form)
print("=== User fills out form ===")
form.username.set_value("jo") # Too short
form.email.set_value("john.example.com") # Invalid
form.password.set_value("pass") # Too short
print("\n=== User corrects input ===")
form.username.set_value("john_doe")
form.email.set_value("john@example.com")
form.password.set_value("SecurePass123")
form.confirm_password.set_value("SecurePass123")
print("\n=== User submits ===")
form.submit_button.click()
Expected Output:
=== User fills out form ===
Username changed to: jo
Validation errors: Username must be at least 3 characters, Invalid email format, Password must be at least 8 characters
Submit is now disabled
Email changed to: john.example.com
Validation errors: Username must be at least 3 characters, Invalid email format, Password must be at least 8 characters
Submit is now disabled
Password changed to: pass
Validation errors: Username must be at least 3 characters, Invalid email format, Password must be at least 8 characters
Submit is now disabled
=== User corrects input ===
Username changed to: john_doe
Validation errors: Invalid email format, Password must be at least 8 characters
Submit is now disabled
Email changed to: john@example.com
Validation errors: Password must be at least 8 characters
Submit is now disabled
Password changed to: SecurePass123
Validation errors: Passwords don't match
Submit is now disabled
Confirm Password changed to: SecurePass123
Form is valid
Submit is now enabled
=== User submits ===
Submit clicked
=== Submitting Registration ===
Username: john_doe
Email: john@example.com
Registration successful!
Key Points:
- Form fields don’t validate each other directly
- Mediator contains all cross-field validation logic
- Button state automatically updates based on form validity
- Easy to add new validation rules without changing field classes
Try it yourself: Add a “Show Password” checkbox that, when checked, reveals the password field content.
Common Mistakes
1. Making the Mediator Too Complex (God Object)
The Mistake: Putting too much logic in the mediator, turning it into a monolithic “god object” that knows everything and does everything.
# BAD: Mediator doing too much
class ChatRoom:
def send_message(self, msg, sender):
# Message validation
if len(msg) > 500:
return False
# Profanity filtering
msg = self._filter_profanity(msg)
# Spam detection
if self._is_spam(sender, msg):
return False
# User permission checking
if not self._has_permission(sender):
return False
# Logging
self._log_message(msg)
# Analytics
self._track_analytics(sender, msg)
# Notification
self._send_push_notifications(msg)
# Distribution
for user in self._users:
user.receive(msg)
Why it’s wrong: The mediator becomes a maintenance nightmare. Every new feature requires modifying it.
Fix: Use additional patterns (Strategy, Chain of Responsibility) to delegate responsibilities:
# GOOD: Mediator delegates to specialized components
class ChatRoom:
def __init__(self):
self._validator = MessageValidator()
self._filter = ProfanityFilter()
self._spam_detector = SpamDetector()
self._logger = MessageLogger()
def send_message(self, msg, sender):
if not self._validator.validate(msg):
return False
msg = self._filter.clean(msg)
if self._spam_detector.is_spam(sender, msg):
return False
self._logger.log(msg)
self._distribute(msg, sender)
2. Colleagues Communicating Directly
The Mistake: Allowing colleagues to hold references to each other, defeating the purpose of the mediator.
# BAD: User has direct reference to another user
class User:
def __init__(self, name, mediator):
self.name = name
self._mediator = mediator
self.friends = [] # Direct references!
def send_private_message(self, friend, msg):
friend.receive(msg) # Bypasses mediator!
Why it’s wrong: Creates the tight coupling the pattern is meant to eliminate.
Fix: All communication must go through the mediator:
# GOOD: All communication through mediator
class User:
def __init__(self, name, mediator):
self.name = name
self._mediator = mediator
def send_private_message(self, recipient_name, msg):
self._mediator.send_private_message(msg, self, recipient_name)
3. Not Defining a Mediator Interface
The Mistake: Skipping the abstract mediator interface and going straight to concrete implementation.
# BAD: No interface, hard to test or swap implementations
class ChatRoom:
def send_message(self, msg, sender):
# Implementation
pass
class User:
def __init__(self, name, chat_room: ChatRoom): # Coupled to concrete class
self._chat_room = chat_room
Why it’s wrong: Makes testing difficult and violates Dependency Inversion Principle.
Fix: Always define an abstract mediator:
# GOOD: Interface allows flexibility
class ChatMediator(ABC):
@abstractmethod
def send_message(self, msg, sender):
pass
class User:
def __init__(self, name, mediator: ChatMediator): # Depends on abstraction
self._mediator = mediator
4. Forgetting to Unregister Colleagues
The Mistake: Not providing a way to remove colleagues from the mediator, causing memory leaks.
# BAD: No way to remove users
class ChatRoom:
def __init__(self):
self._users = []
def add_user(self, user):
self._users.append(user)
# No remove_user method!
Why it’s wrong: Users who leave still consume memory and receive messages.
Fix: Always provide registration and unregistration:
# GOOD: Proper lifecycle management
class ChatRoom:
def __init__(self):
self._users = []
def add_user(self, user):
self._users.append(user)
def remove_user(self, user):
if user in self._users:
self._users.remove(user)
print(f"{user.name} left the chat")
5. Using Mediator When Simple Direct Communication Suffices
The Mistake: Applying the pattern when you only have 2-3 objects with simple interactions.
# BAD: Overkill for simple case
class LoginMediator:
def __init__(self):
self.username_field = None
self.password_field = None
def notify(self, sender, event):
# Complex coordination for just two fields
pass
Why it’s wrong: Adds unnecessary complexity. The pattern is for complex, many-to-many relationships.
Fix: Use direct communication for simple cases:
# GOOD: Simple direct interaction
class LoginForm:
def __init__(self):
self.username = TextField()
self.password = TextField()
def validate(self):
return bool(self.username.value and self.password.value)
Rule of thumb: Use Mediator when you have 4+ objects with complex interaction patterns, or when adding new objects would require changing many existing ones.
Interview Tips
What Interviewers Look For
1. Understanding of the Core Problem
Interviewers want to see you identify when tight coupling is a problem. Be ready to explain:
- “The Mediator Pattern is useful when objects have many-to-many relationships that create maintenance problems”
- Draw a before/after diagram showing how n objects with direct communication have O(n²) connections, but with a mediator it’s O(n)
2. Ability to Identify Appropriate Use Cases
Common interview question: “When would you use the Mediator Pattern?”
Strong answer structure:
- “I’d use it when [specific scenario], for example…”
- Give concrete examples: chat systems, GUI dialogs, air traffic control
- Mention the trade-off: “It centralizes control but can become a bottleneck”
Weak answer: “When objects need to communicate” (too vague)
Common Interview Scenarios
Scenario 1: Design a Chat Application
This is a classic mediator problem. Show you understand:
- Users shouldn’t know about each other
- The chat room mediates all communication
- Easy to add features (private messages, rooms) without changing User class
What to say: “I’d use a ChatRoom mediator so users don’t need references to each other. This makes it easy to add features like message history or user blocking without modifying the User class.”
Scenario 2: GUI Component Coordination
Example: “Design a form where enabling a checkbox disables certain fields.”
Strong approach:
- Identify this as a coordination problem
- Suggest a FormMediator that handles field interactions
- Explain how this keeps field classes simple and reusable
Code Implementation Tips
1. Start with the Interface
When asked to implement, always begin with the mediator interface:
class Mediator(ABC):
@abstractmethod
def notify(self, sender, event):
pass
This shows you understand abstraction and testability.
2. Show Bidirectional Setup
Demonstrate that colleagues need a reference to the mediator:
class Colleague:
def __init__(self, mediator: Mediator):
self._mediator = mediator
self._mediator.register(self) # Important!
3. Handle Edge Cases
Mention considerations like:
- What if a colleague is removed? (unregistration)
- Thread safety for concurrent access
- Circular notification prevention
Comparison Questions
“How is Mediator different from Observer?”
Strong answer:
- “Observer is one-to-many (one subject, many observers)”
- “Mediator is many-to-many (colleagues communicate through mediator)”
- “Observer is about notification; Mediator is about coordination”
- “They can work together: mediator can use observer to notify colleagues”
“How is Mediator different from Facade?”
Strong answer:
- “Facade simplifies a complex subsystem with a unified interface”
- “Mediator coordinates communication between colleagues”
- “Facade is structural; Mediator is behavioral”
- “In Facade, subsystem classes don’t know about the facade; in Mediator, colleagues know about the mediator”
Red Flags to Avoid
❌ “Mediator is just a middleman class” — Too simplistic, doesn’t show understanding of the coupling problem
❌ “Use it whenever objects communicate” — Shows lack of judgment about when pattern is appropriate
❌ Implementing mediator without an interface — Suggests you don’t understand abstraction
❌ Making the mediator do everything — Shows you don’t recognize the god object anti-pattern
Bonus Points
✅ Mention real-world frameworks: “Spring’s ApplicationContext acts as a mediator for beans”
✅ Discuss trade-offs: “The mediator can become a bottleneck, so sometimes you need to split it into multiple mediators”
✅ Suggest complementary patterns: “I’d combine Mediator with Observer for event notification” or “Use Strategy pattern to handle different coordination rules”
✅ Show testing awareness: “The mediator interface makes it easy to create a mock for testing colleagues in isolation”
Practice Question
“Design a system where multiple sensors (temperature, humidity, pressure) trigger actions on multiple actuators (heater, fan, alarm). How would you structure this?”
Strong answer approach:
- Identify this as a many-to-many coordination problem
- Propose a ControlSystemMediator
- Explain that sensors notify the mediator of readings
- Mediator contains logic to coordinate actuator responses
- Mention this makes it easy to add new sensors or actuators
- Draw a quick diagram showing the structure
This demonstrates you can apply the pattern to novel problems, not just recite textbook examples.
Key Takeaways
-
The Mediator Pattern decouples objects by making them communicate through a central mediator instead of directly referencing each other, reducing dependencies from O(n²) to O(n).
-
Use it for complex coordination: Apply the pattern when you have multiple objects with intricate interaction patterns, not for simple two-object relationships. Classic use cases include chat systems, GUI forms, and air traffic control.
-
Mediator centralizes control logic: All coordination rules live in one place, making the system easier to understand and modify. However, be careful not to create a “god object” — delegate complex responsibilities to specialized components.
-
Colleagues only know the mediator: Objects (colleagues) should never hold direct references to each other. All communication flows through the mediator interface, enabling loose coupling and easier testing.
-
Trade-offs matter: While the mediator simplifies colleague classes and reduces coupling, it can become a bottleneck or single point of failure. Consider splitting into multiple mediators for large systems, and always define an abstract mediator interface for flexibility.