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"]
    end

The 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
ComponentRoleCharacteristics
Mediator interfaceContract for all mediatorsNotify and Register methods
Concrete MediatorHolds references to all colleagues; implements coordination logicCan bloat if not kept in check
Colleague interfaceContract for all components communicating via the mediatorOnly knows the mediator, not other colleagues
Concrete ColleagueCalls mediator.Notify() on events; reacts when the mediator calls its methodsHolds 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
AspectObserverMediator
Who coordinatesNo one — the Subject broadcasts, Observers react on their ownThe Mediator actively manages who gets what
Knowledge of rulesObservers filter themselvesThe Mediator determines routing
Number of subjectsUsually one subject, many observersMany components, all talking to one mediator
CouplingSubject-Observer loosely coupledAll colleagues only know the mediator
Best forEvent broadcasting, notificationsComplex 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.

← Previous: Template Method   Next: Memento →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact