Mediator Pattern #
Imagine a flight booking form: the date field affects the list of available flights, the class choice affects the price and seat availability, the promo code affects the final price, the passenger count affects the total, and the “Buy” button is only enabled when all fields are valid. Without central coordination, every component has to know about every other component it affects — the date field must hold a reference to the flight dropdown, the flight dropdown must hold a reference to the price component, and so on until a tangled dependency web forms. When a small change happens, nobody knows which components need updating. The Mediator Pattern cuts through all this cross-dependency by introducing a single coordinator object — every component only talks to the Mediator, and the Mediator decides who needs to know what.
What Is the Mediator Pattern? #
The Mediator Pattern is a behavioral design pattern that defines an object that centralizes communication coordination between components, so the components do not need to know each other directly. Instead of many direct connections between components (which can grow quadratically in number), every component has only one connection to the mediator.
Two properties define the Mediator Pattern:
- Total decoupling — components (called colleagues) hold no references to other components; they only know the mediator
- Centralized coordination logic — the rule “if X changes, Y and Z must be updated” lives in one place, not scattered across each component
flowchart LR
subgraph "Without Mediator — N² connections"
A1[Comp A] <--> B1[Comp B]
A1 <--> C1[Comp C]
A1 <--> D1[Comp D]
B1 <--> C1
B1 <--> D1
C1 <--> D1
note1["4 components = 6 connections\\n10 components = 45 connections"]
end
subgraph "With Mediator — N connections"
A2[Comp A] --> M[Mediator]
B2[Comp B] --> M
C2[Comp C] --> M
D2[Comp D] --> M
M --> A2
M --> B2
M --> C2
M --> D2
note2["4 components = 4 connections\\n10 components = 10 connections"]
endThe Problem It Solves #
The Mediator’s benefit is most obvious when many components affect each other — because the number of connections grows quadratically without a mediator, but only linearly with one.
The Problem: Spaghetti Coupling #
// ANTI-PATTERN: every component holds references to other components
type InventoryComponent struct {
priceComp *PriceComponent // knows about price
orderComp *OrderComponent // knows about order
notifComp *NotifComponent // knows about notifications
}
func (ic *InventoryComponent) OnStockUpdate(productID string, qty int) {
// Must know how to update every other component directly
ic.priceComp.RecalculateForProduct(productID)
if qty == 0 {
ic.priceComp.MarkOutOfStock(productID)
ic.orderComp.BlockNewOrders(productID)
ic.notifComp.NotifySubscribers(productID, "out_of_stock")
}
}
// Every component has dependencies on every other component
// Adding a new component = updating every component that needs to know about it
// CORRECT: every component only knows the mediator
type InventoryComponent struct {
mediator Mediator
}
func (ic *InventoryComponent) OnStockUpdate(productID string, qty int) {
ic.mediator.Notify(ic, "stock_updated", map[string]interface{}{
"product_id": productID,
"quantity": qty,
})
// The component does not know who will react or how
}
Structure and Components #
classDiagram
class Mediator {
<<interface>>
+Notify(sender Colleague, event string, data any)
+Register(colleague Colleague)
}
class Colleague {
<<interface>>
+SetMediator(mediator Mediator)
+ComponentName() string
}
class WorkflowMediator {
-colleagues map string Colleague
-rules map string HandlerList
+Notify(sender Colleague, event string, data any)
+Register(colleague Colleague)
}
class InventoryComponent {
-mediator Mediator
+SetMediator(mediator Mediator)
+UpdateStock(productID string, qty int)
+ComponentName() string
}
class PriceComponent {
-mediator Mediator
+SetMediator(mediator Mediator)
+ComponentName() string
}
Mediator <|.. WorkflowMediator
Colleague <|.. InventoryComponent
Colleague <|.. PriceComponent
WorkflowMediator o-- Colleague : manages
InventoryComponent --> Mediator : notifies
PriceComponent --> Mediator : notifies| Component | Role | Characteristics |
|---|---|---|
| Mediator interface | Contract for all mediators | Notify and Register methods |
| Concrete Mediator | Holds references to all colleagues; implements coordination logic | Can bloat if not kept in check |
| Colleague interface | Contract for all components communicating via the mediator | Only knows the mediator, not other colleagues |
| Concrete Colleague | Calls mediator.Notify() on events; reacts when the mediator calls its methods | Holds no references to other colleagues |
Full Implementation: Chatroom #
The chatroom is a classic Mediator Pattern example — every user does not need to know about other users; they only know the chatroom as the mediator.
Mediator Interface and Colleague #
package chat
import (
"fmt"
"strings"
"time"
)
// Mediator is the interface for all chatroom mediators.
type Mediator interface {
SendMessage(sender *User, message string)
SendPrivateMessage(sender *User, recipientName, message string) error
Register(user *User)
Unregister(username string)
ListUsers() []string
}
// User is a Colleague — it communicates through the mediator, not directly to other users.
type User struct {
Name string
mediator Mediator
inbox []Message
}
// Message represents one message in the chatroom.
type Message struct {
From string
To string // empty = broadcast
Content string
SentAt time.Time
IsPrivate bool
}
func NewUser(name string) *User {
return &User{
Name: name,
inbox: make([]Message, 0),
}
}
// SetMediator registers the mediator with the user.
func (u *User) SetMediator(m Mediator) {
u.mediator = m
m.Register(u)
}
// Send sends a message to all users in the chatroom.
func (u *User) Send(message string) {
u.mediator.SendMessage(u, message)
}
// SendTo sends a private message to a specific user.
func (u *User) SendTo(recipientName, message string) error {
return u.mediator.SendPrivateMessage(u, recipientName, message)
}
// Receive is called by the mediator when a message arrives.
// The user does not call this itself — it is a "callback" from the mediator.
func (u *User) Receive(msg Message) {
u.inbox = append(u.inbox, msg)
if msg.IsPrivate {
fmt.Printf("[DM to %s] %s → %s: %s\n", u.Name, msg.From, u.Name, msg.Content)
} else {
fmt.Printf("[%s] %s: %s\n", u.Name, msg.From, msg.Content)
}
}
// Inbox returns all messages received by the user.
func (u *User) Inbox() []Message { return u.inbox }
Concrete Mediator: ChatRoom #
package chat
import (
"fmt"
"sync"
"time"
)
// ChatRoom is the Concrete Mediator — it manages all communication between users.
type ChatRoom struct {
mu sync.RWMutex
users map[string]*User
log []Message
}
func NewChatRoom() *ChatRoom {
return &ChatRoom{
users: make(map[string]*User),
log: make([]Message, 0),
}
}
// Register adds a new user to the chatroom.
func (c *ChatRoom) Register(user *User) {
c.mu.Lock()
defer c.mu.Unlock()
c.users[user.Name] = user
fmt.Printf("[ChatRoom] %s joined. Total users: %d\n", user.Name, len(c.users))
// Notify the other users that a new user joined
joinMsg := Message{
From: "ChatRoom",
Content: fmt.Sprintf("%s joined the chatroom", user.Name),
SentAt: time.Now(),
}
for name, u := range c.users {
if name != user.Name {
u.Receive(joinMsg)
}
}
}
// Unregister removes a user from the chatroom.
func (c *ChatRoom) Unregister(username string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.users, username)
fmt.Printf("[ChatRoom] %s left. Total users: %d\n", username, len(c.users))
leaveMsg := Message{
From: "ChatRoom",
Content: fmt.Sprintf("%s left the chatroom", username),
SentAt: time.Now(),
}
for _, u := range c.users {
u.Receive(leaveMsg)
}
}
// SendMessage broadcasts a message to all users except the sender.
// This is the core of mediator coordination.
func (c *ChatRoom) SendMessage(sender *User, content string) {
c.mu.RLock()
defer c.mu.RUnlock()
msg := Message{
From: sender.Name,
Content: content,
SentAt: time.Now(),
}
c.log = append(c.log, msg)
// Send to all users EXCEPT the sender
for name, user := range c.users {
if name != sender.Name {
user.Receive(msg)
}
}
}
// SendPrivateMessage sends a message only to one specific user.
func (c *ChatRoom) SendPrivateMessage(sender *User, recipientName, content string) error {
c.mu.RLock()
defer c.mu.RUnlock()
recipient, ok := c.users[recipientName]
if !ok {
return fmt.Errorf("user %q not found in chatroom", recipientName)
}
msg := Message{
From: sender.Name,
To: recipientName,
Content: content,
SentAt: time.Now(),
IsPrivate: true,
}
c.log = append(c.log, msg)
recipient.Receive(msg)
return nil
}
// ListUsers returns the list of active usernames.
func (c *ChatRoom) ListUsers() []string {
c.mu.RLock()
defer c.mu.RUnlock()
names := make([]string, 0, len(c.users))
for name := range c.users {
names = append(names, name)
}
return names
}
// MessageLog returns the entire message log.
func (c *ChatRoom) MessageLog() []Message {
c.mu.RLock()
defer c.mu.RUnlock()
result := make([]Message, len(c.log))
copy(result, c.log)
return result
}
Demonstration #
func main() {
room := chat.NewChatRoom()
alice := chat.NewUser("Alice")
bob := chat.NewUser("Bob")
citra := chat.NewUser("Citra")
// Register with the chatroom — users do not know each other
alice.SetMediator(room)
bob.SetMediator(room)
citra.SetMediator(room)
fmt.Println("\n--- Broadcast Messages ---")
alice.Send("Hello everyone!")
bob.Send("Hi Alice!")
fmt.Println("\n--- Private Messages ---")
_ = alice.SendTo("Citra", "Hi Citra, got a moment to chat?")
_ = bob.SendTo("NonExistent", "test") // error — user does not exist
fmt.Printf("\nTotal users: %v\n", room.ListUsers())
fmt.Printf("Alice inbox: %d messages\n", len(alice.Inbox()))
}
Second Case Study: Workflow Engine #
A workflow engine is a more complex Mediator use case — each step in the workflow does not need to know about other steps; the workflow coordinator (mediator) decides which step should run next.
package workflow
import (
"context"
"fmt"
"log/slog"
)
// WorkflowStep is a Colleague — one step in the workflow.
type WorkflowStep interface {
Execute(ctx context.Context, data map[string]interface{}) error
StepName() string
SetCoordinator(coordinator WorkflowCoordinator)
}
// WorkflowCoordinator is the Mediator — it manages the order and execution conditions of steps.
type WorkflowCoordinator interface {
TriggerStep(ctx context.Context, stepName string, data map[string]interface{}) error
OnStepComplete(step WorkflowStep, data map[string]interface{})
OnStepFailed(step WorkflowStep, err error)
RegisterStep(step WorkflowStep)
}
// BaseStep provides the default implementation for SetCoordinator.
type BaseStep struct {
coordinator WorkflowCoordinator
}
func (s *BaseStep) SetCoordinator(c WorkflowCoordinator) { s.coordinator = c }
// OrderFulfillmentCoordinator orchestrates the order fulfillment process.
// Each step only needs to know the coordinator, not other steps.
type OrderFulfillmentCoordinator struct {
steps map[string]WorkflowStep
logger *slog.Logger
// Transitions controlled by the mediator: when step A completes, run step B
transitions map[string][]string
}
func NewOrderFulfillmentCoordinator(logger *slog.Logger) *OrderFulfillmentCoordinator {
c := &OrderFulfillmentCoordinator{
steps: make(map[string]WorkflowStep),
logger: logger,
transitions: map[string][]string{
"validate_order": {"reserve_inventory"},
"reserve_inventory": {"process_payment"},
"process_payment": {"send_confirmation", "update_analytics"},
"send_confirmation": {}, // terminal — no next step
"update_analytics": {}, // terminal
},
}
return c
}
// RegisterStep registers a step with the coordinator.
func (c *OrderFulfillmentCoordinator) RegisterStep(step WorkflowStep) {
step.SetCoordinator(c)
c.steps[step.StepName()] = step
}
// TriggerStep runs a specific step.
func (c *OrderFulfillmentCoordinator) TriggerStep(ctx context.Context, stepName string, data map[string]interface{}) error {
step, ok := c.steps[stepName]
if !ok {
return fmt.Errorf("step %q is not registered", stepName)
}
c.logger.InfoContext(ctx, "executing step", "step", stepName)
if err := step.Execute(ctx, data); err != nil {
c.OnStepFailed(step, err)
return err
}
return nil
}
// OnStepComplete is called after a step succeeds.
// The coordinator decides which step should run next.
func (c *OrderFulfillmentCoordinator) OnStepComplete(step WorkflowStep, data map[string]interface{}) {
nextSteps, ok := c.transitions[step.StepName()]
if !ok || len(nextSteps) == 0 {
c.logger.Info("workflow branch complete", "last_step", step.StepName())
return
}
ctx := context.Background()
for _, nextStep := range nextSteps {
c.logger.InfoContext(ctx, "triggering next step",
"from", step.StepName(), "to", nextStep)
if err := c.TriggerStep(ctx, nextStep, data); err != nil {
c.logger.ErrorContext(ctx, "step failed in chain",
"step", nextStep, "error", err)
}
}
}
// OnStepFailed is called when a step fails.
func (c *OrderFulfillmentCoordinator) OnStepFailed(step WorkflowStep, err error) {
c.logger.Error("step failed",
"step", step.StepName(),
"error", err,
)
// The coordinator can decide: retry? rollback? alert?
}
// Start begins the workflow from the first step.
func (c *OrderFulfillmentCoordinator) Start(ctx context.Context, data map[string]interface{}) error {
return c.TriggerStep(ctx, "validate_order", data)
}
// Concrete Steps — each one knows nothing about the other steps
type ValidateOrderStep struct {
BaseStep
}
func (s *ValidateOrderStep) StepName() string { return "validate_order" }
func (s *ValidateOrderStep) Execute(ctx context.Context, data map[string]interface{}) error {
orderID, ok := data["order_id"].(string)
if !ok || orderID == "" {
return fmt.Errorf("order_id is required")
}
fmt.Printf("[ValidateOrder] Validating order: %s\n", orderID)
// ... validation ...
s.coordinator.OnStepComplete(s, data) // notify the coordinator that this step finished
return nil
}
type ReserveInventoryStep struct {
BaseStep
inventorySvc InventoryService
}
func (s *ReserveInventoryStep) StepName() string { return "reserve_inventory" }
func (s *ReserveInventoryStep) Execute(ctx context.Context, data map[string]interface{}) error {
fmt.Printf("[ReserveInventory] Reserving stock for order: %s\n", data["order_id"])
reservationID, err := s.inventorySvc.Reserve(ctx, data["items"])
if err != nil {
return fmt.Errorf("reservation failed: %w", err)
}
data["reservation_id"] = reservationID
s.coordinator.OnStepComplete(s, data)
return nil
}
func (s *ReserveInventoryStep) SetCoordinator(c WorkflowCoordinator) { s.coordinator = c }
// ... other steps follow the same pattern
// Assembly
func SetupFulfillmentWorkflow(logger *slog.Logger, inventorySvc InventoryService) *OrderFulfillmentCoordinator {
coord := NewOrderFulfillmentCoordinator(logger)
coord.RegisterStep(&ValidateOrderStep{})
coord.RegisterStep(&ReserveInventoryStep{inventorySvc: inventorySvc})
// ... register the other steps
return coord
}
Typed Event Mediator: The Modern Approach #
A more type-safe version of the Mediator uses generics or typed events to avoid type assertions in every handler.
package mediator
import (
"fmt"
"sync"
)
// Event is the interface implemented by every event.
type Event interface {
EventName() string
}
// Handler is the function handling a specific event.
type Handler[T Event] func(event T) error
// TypedMediator is a mediator with type-safe event handling.
type TypedMediator struct {
mu sync.RWMutex
handlers map[string][]interface{} // map[eventName][]handlerFunc
}
func NewTypedMediator() *TypedMediator {
return &TypedMediator{
handlers: make(map[string][]interface{}),
}
}
// Subscribe registers a handler for a specific event.
func Subscribe[T Event](m *TypedMediator, handler Handler[T]) {
var zero T
eventName := zero.EventName()
m.mu.Lock()
defer m.mu.Unlock()
m.handlers[eventName] = append(m.handlers[eventName], handler)
}
// Publish sends an event to all registered handlers.
func Publish[T Event](m *TypedMediator, event T) []error {
m.mu.RLock()
handlers := m.handlers[event.EventName()]
m.mu.RUnlock()
var errs []error
for _, h := range handlers {
if typedHandler, ok := h.(Handler[T]); ok {
if err := typedHandler(event); err != nil {
errs = append(errs, err)
}
}
}
return errs
}
// Example usage of the typed mediator
type UserRegisteredEvent struct {
UserID string
Email string
Username string
}
func (e UserRegisteredEvent) EventName() string { return "user.registered" }
type OrderCreatedEvent struct {
OrderID string
UserID string
Amount float64
}
func (e OrderCreatedEvent) EventName() string { return "order.created" }
func setupMediator() {
m := NewTypedMediator()
// Type-safe: the handler receives UserRegisteredEvent, not interface{}
Subscribe(m, func(event UserRegisteredEvent) error {
fmt.Printf("Sending welcome email to %s\n", event.Email)
return nil
})
Subscribe(m, func(event UserRegisteredEvent) error {
fmt.Printf("Setting up user profile for %s\n", event.Username)
return nil
})
Subscribe(m, func(event OrderCreatedEvent) error {
fmt.Printf("Processing order %s worth Rp %.0f\n", event.OrderID, event.Amount)
return nil
})
// Publish — type-safe, no type assertions needed in handlers
errs := Publish(m, UserRegisteredEvent{
UserID: "user-1", Email: "[email protected]", Username: "alice",
})
for _, err := range errs {
fmt.Printf("Handler error: %v\n", err)
}
}
Mediator vs Observer: When to Use Which #
This is the most frequently asked comparison. Both involve notification between objects, but the mechanism and purpose differ.
flowchart TD
subgraph "Observer"
S[Subject] -->|broadcast\\nwithout coordination| O1[Observer 1]
S -->|broadcast| O2[Observer 2]
S -->|broadcast| O3[Observer 3]
note_obs["The Subject does not know what\\nthe Observers do with its event"]
end
subgraph "Mediator"
C1[Comp A] -->|notify| M[Mediator]
M -->|selectively notify\\nbased on rules| C2[Comp B]
M --> C3[Comp C]
M -. "not notified\\n(the mediator decides)" .-> C4[Comp D]
note_med["The Mediator knows the rules:\\nwho needs to know what"]
end| Aspect | Observer | Mediator |
|---|---|---|
| Who coordinates | No one — the Subject broadcasts, Observers react on their own | The Mediator actively manages who gets what |
| Knowledge of rules | Observers filter themselves | The Mediator determines routing |
| Number of subjects | Usually one subject, many observers | Many components, all talking to one mediator |
| Coupling | Subject-Observer loosely coupled | All colleagues only know the mediator |
| Best for | Event broadcasting, notifications | Complex multi-component coordination |
The Danger: Mediator as a God Object #
The Mediator is the pattern most vulnerable to becoming a God Object — one object that knows and handles everything. This removes the very benefit it should provide.
// ANTI-PATTERN: a Mediator doing business logic
func (m *OrderMediator) Notify(sender Colleague, event string, data any) {
switch event {
case "stock_updated":
// The mediator directly calculates prices — this is business logic!
if qty, ok := data.(int); ok && qty < 10 {
newPrice := m.currentPrice * 1.2 // 20% markup when stock is low
m.priceComp.SetPrice(newPrice)
}
// ...
case "order_paid":
// The mediator directly updates analytics — also not its job!
m.analyticsDB.Insert("purchases", data)
}
}
// CORRECT: the Mediator only routes, logic lives in the components
func (m *OrderMediator) Notify(sender Colleague, event string, data any) {
switch event {
case "stock_updated":
// The mediator only routes — "tell the price component about the stock update"
m.priceComp.OnStockUpdated(data)
case "order_paid":
// The mediator only routes — it does not know what analytics does
m.analyticsComp.OnOrderPaid(data)
}
// Logic lives in each component, not in the mediator
}
Signs a mediator has grown too large and needs to be split:
A healthy Mediator:
✓ Only contains routing logic ("send this there")
✓ Contains no business logic
✓ Its methods are short and simple
✓ Can be tested without complex mocks
A Mediator that has become a God Object:
✗ Contains business calculations
✗ Accesses the database directly
✗ Has methods with hundreds of lines of code
✗ Hard to test because of too many dependencies
The solution when a mediator is too large: split it into several mediators, each responsible for one domain.
Testing the Mediator Pattern #
// MockColleague for testing a mediator
type MockColleague struct {
name string
mediator Mediator
receivedEvents []struct {
event string
data interface{}
}
}
func (m *MockColleague) ComponentName() string { return m.name }
func (m *MockColleague) SetMediator(med Mediator) { m.mediator = med }
func (m *MockColleague) Notify(event string, data any) {
m.mediator.Notify(m, event, data)
}
func (m *MockColleague) OnNotified(event string, data any) {
m.receivedEvents = append(m.receivedEvents, struct {
event string
data interface{}
}{event, data})
}
func (m *MockColleague) EventCount() int { return len(m.receivedEvents) }
func TestChatRoom_BroadcastToAllExceptSender(t *testing.T) {
room := chat.NewChatRoom()
alice := chat.NewUser("Alice")
bob := chat.NewUser("Bob")
citra := chat.NewUser("Citra")
alice.SetMediator(room)
bob.SetMediator(room)
citra.SetMediator(room)
alice.Send("Hello everyone!")
// Bob and Citra must receive it, Alice must not
if len(bob.Inbox()) != 1 {
t.Errorf("Bob should receive 1 message, got %d", len(bob.Inbox()))
}
if len(citra.Inbox()) != 1 {
t.Errorf("Citra should receive 1 message, got %d", len(citra.Inbox()))
}
if len(alice.Inbox()) != 0 {
t.Errorf("Alice should NOT receive her own message")
}
}
func TestChatRoom_PrivateMessage_OnlyToRecipient(t *testing.T) {
room := chat.NewChatRoom()
alice := chat.NewUser("Alice")
bob := chat.NewUser("Bob")
citra := chat.NewUser("Citra")
alice.SetMediator(room)
bob.SetMediator(room)
citra.SetMediator(room)
err := alice.SendTo("Bob", "Private message for Bob only")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(bob.Inbox()) != 1 {
t.Errorf("Bob should receive private message, got %d", len(bob.Inbox()))
}
if !bob.Inbox()[0].IsPrivate {
t.Error("message should be marked as private")
}
if len(citra.Inbox()) != 0 {
t.Error("Citra should NOT receive private message between Alice and Bob")
}
}
func TestChatRoom_UnknownRecipient_ReturnsError(t *testing.T) {
room := chat.NewChatRoom()
alice := chat.NewUser("Alice")
alice.SetMediator(room)
err := alice.SendTo("NonExistentUser", "test")
if err == nil {
t.Error("expected error for unknown recipient")
}
}
func TestChatRoom_Unregister_StopsMessages(t *testing.T) {
room := chat.NewChatRoom()
alice := chat.NewUser("Alice")
bob := chat.NewUser("Bob")
alice.SetMediator(room)
bob.SetMediator(room)
room.Unregister("Bob")
alice.Send("Hello Bob!")
// Bob has left, so he should not receive new messages
if len(bob.Inbox()) != 0 {
t.Errorf("Bob should not receive message after unregistering")
}
}
When to Use and When Not to #
USE Mediator if:
✓ Many components interact and the number of connections grows out of control
✓ Coordination logic between components needs to be centralized in one place
✓ Adding a new component must not affect existing components
✓ You are building a chatroom, workflow engine, complex UI form, or event bus
✓ You want to be able to change coordination rules without changing the components
AVOID Mediator if:
✗ Only 2-3 components interact — direct coupling is simpler
✗ Components interact in very different ways — the mediator will become a God Object
✗ Coordination rules will never change — excessive abstraction
✗ The mediator has already become a God Object — a sign the mediator's domain is too broad
Mediator Review Checklist #
DESIGN:
□ Every colleague only holds a reference to the mediator, not to other colleagues
□ The Mediator only contains routing logic, not business logic
□ Colleagues do not know who the mediator will notify
□ Adding a new colleague does not change existing colleagues
IMPLEMENTATION:
□ Thread-safe: Register, Unregister, Notify use a mutex
□ The Mediator copies the colleague list before iterating to avoid deadlock
□ Calls to colleagues are wrapped with recovery to avoid panic cascades
MEDIATOR SIZE:
□ The Notify method has no more than 10-15 cases
□ No business calculations inside the Mediator
□ The Mediator can be tested without many complex mocks
TESTING:
□ Test broadcast: all colleagues that should receive, do receive
□ Test selective notify: irrelevant colleagues do not receive
□ Test Unregister: unregistered colleagues do not receive
□ Test error handling: a panic in one colleague does not crash the others
Summary #
- The Mediator centralizes coordination logic — components do not need to know each other; everything talks to one mediator that manages who needs to know what.
- N connections, not N² — without a mediator, 10 components can have 45 connections; with a mediator, only 10; this is the biggest benefit as the system grows.
- Mediator vs Observer: Observer broadcasts without active coordination; Mediator manages routing explicitly based on rules stored inside it.
- Typed Event Mediator — the modern generics-based version eliminates type assertions in handlers; safer and easier to refactor.
- A Mediator must be thin — only routing logic, not business logic; if the Notify method is already hundreds of lines, it is a God Object that needs splitting.
- A workflow engine is a Mediator — the coordinator decides which step runs based on the previous step’s result; each step only knows the coordinator.
- A chatroom is the classic use case — users do not know each other; all messages go through the ChatRoom, which decides who receives what.
- Split an oversized mediator — different domains should have their own mediators; one mediator for all domains is a recipe for a God Object.