StackPractices
intermediate By Mathias Paulenko

Mediator Pattern for Loose Component Coupling in

Reduce chaotic dependencies between UI components by introducing a mediator that centralizes communication, preventing explicit references between peers

The Mediator pattern defines an object that encapsulates how a set of objects interact. Instead of components referring to each other directly, they refer to a mediator, reducing the number of explicit connections from many-to-many to many-to-one. This is essential for complex UIs where dozens of components need to stay synchronized.

When to Use This

  • Components have many-to-many relationships that would otherwise create tight coupling
  • Reusing components independently is difficult because they depend on specific peers
  • Communication logic is scattered and hard to test

Problem

A dashboard with filters, charts, tables, and maps requires each widget to notify every other widget when data changes. Each widget holds references to 5-6 others, creating a dependency nightmare.

Solution

// mediator/DashboardMediator.ts
interface Mediator {
  notify(sender: Component, event: string, data?: unknown): void;
}

abstract class Component {
  constructor(protected mediator: Mediator) {}

  send(event: string, data?: unknown): void {
    this.mediator.notify(this, event, data);
  }
}

class FilterPanel extends Component {
  private selectedRegion = 'all';

  selectRegion(region: string): void {
    this.selectedRegion = region;
    this.send('region-changed', region);
  }
}

class ChartWidget extends Component {
  private data: unknown[] = [];

  updateData(data: unknown[]): void {
    this.data = data;
    this.render();
  }

  private render(): void {
    console.log('Chart rendered with', this.data.length, 'points');
  }
}

class TableWidget extends Component {
  private rows: unknown[] = [];

  updateRows(rows: unknown[]): void {
    this.rows = rows;
    console.log('Table updated with', rows.length, 'rows');
  }
}

class MapWidget extends Component {
  private center = { lat: 0, lng: 0 };

  panTo(center: { lat: number; lng: number }): void {
    this.center = center;
    console.log('Map centered at', center);
  }
}

// Mediator orchestrates all communication
class DashboardMediator implements Mediator {
  private filters: FilterPanel;
  private chart: ChartWidget;
  private table: TableWidget;
  private map: MapWidget;

  setComponents(
    filters: FilterPanel,
    chart: ChartWidget,
    table: TableWidget,
    map: MapWidget
  ): void {
    this.filters = filters;
    this.chart = chart;
    this.table = table;
    this.map = map;
  }

  notify(sender: Component, event: string, data?: unknown): void {
    switch (event) {
      case 'region-changed': {
        const filteredData = this.fetchDataForRegion(data as string);
        this.chart.updateData(filteredData);
        this.table.updateRows(filteredData);
        this.map.panTo(this.getRegionCenter(data as string));
        break;
      }
      case 'chart-point-clicked': {
        const point = data as { lat: number; lng: number };
        this.map.panTo(point);
        break;
      }
    }
  }

  private fetchDataForRegion(region: string): unknown[] {
    return [{ id: 1, region }];
  }

  private getRegionCenter(region: string): { lat: number; lng: number } {
    const centers: Record<string, { lat: number; lng: number }> = {
      'north': { lat: 45, lng: 0 },
      'south': { lat: -45, lng: 0 },
    };
    return centers[region] || { lat: 0, lng: 0 };
  }
}

// Usage
const mediator = new DashboardMediator();
const filters = new FilterPanel(mediator);
const chart = new ChartWidget(mediator);
const table = new TableWidget(mediator);
const map = new MapWidget(mediator);

mediator.setComponents(filters, chart, table, map);
filters.selectRegion('north');

Variation: Event Bus Mediator

// mediator/EventBus.ts
class EventBus implements Mediator {
  private listeners = new Map<string, Set<(data: unknown) => void>>();

  subscribe(event: string, callback: (data: unknown) => void): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);

    return () => this.listeners.get(event)?.delete(callback);
  }

  notify(_sender: Component, event: string, data?: unknown): void {
    this.listeners.get(event)?.forEach(cb => cb(data));
  }

  emit(event: string, data?: unknown): void {
    this.notify(null as unknown as Component, event, data);
  }
}

const bus = new EventBus();
bus.subscribe('user-login', user => console.log('Logged in:', user));
bus.emit('user-login', { id: 1 });

How It Works

  1. Mediator declares the communication interface
  2. Concrete Mediator implements coordination logic between colleagues
  3. Colleague components send events to the mediator instead of each other
  4. Client creates and wires the mediator with all colleagues

Production Considerations

  • Keep mediators focused on one domain; do not create a god object
  • Use typed events to prevent stringly-typed communication bugs
  • Consider state management libraries (Redux, Zustand) as evolved mediators. See Singleton for service instance management.

Common Mistakes

  • Creating a mediator so large it becomes unmaintainable
  • Bypassing the mediator for direct component communication
  • Not unsubscribing event listeners, causing memory leaks

Troubleshooting

  • Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
  • Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
  • Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
  • Tests break when the design changes: favor stable contracts over internal structure.
  • Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.

Further Reading

  • Official documentation: check the current reference for the framework or tool used.
  • Related guides: explore the mediator and behavioral-patterns guides for deeper coverage.
  • Complementary patterns: review design patterns applicable to your technology stack.
  • Public postmortems: study real incidents from teams that faced similar production issues.

Production Notes

  • Deploy gradually using canary or blue-green to catch regressions early.
  • Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
  • Document the rollback in the runbook; test the procedure in staging at least once per quarter.
  • Review structured logs with correlation IDs to trace requests end-to-end during incidents.

Key Takeaways

  • Apply mediator pattern for loose component coupling in when you need a practical solution for your use case.
  • Monitor performance after implementation; measure latency, errors, and resource usage before and after.
  • Check the Troubleshooting section for common failures; most have documented root causes with fixes.
  • Keep dependencies updated and run tests in CI to prevent production regressions.

Advanced Topics

Scenario: Mediator for Chat Room

// Mediator pattern: centralize communication between components
interface ChatMediator {
  sendMessage(msg: string, user: User): void;
  registerUser(user: User): void;
}

abstract class User {
  constructor(protected mediator: ChatMediator, public name: string) {}
  abstract send(msg: string): void;
  abstract receive(msg: string): void;
}

class ChatUser extends User {
  send(msg: string) { this.mediator.sendMessage(msg, this); }
  receive(msg: string) { console.log(`[${this.name}] received: ${msg}`); }
}

// Concrete mediator
class ChatRoom implements ChatMediator {
  private users: User[] = [];

  registerUser(user: User) { this.users.push(user); }

  sendMessage(msg: string, sender: User) {
    // Broadcast to all except sender
    this.users.filter(u => u !== sender).forEach(u => u.receive(msg));
  }
}

// Usage: users do not know each other
const chat = new ChatRoom();
const alice = new ChatUser(chat, "Alice");
const bob = new ChatUser(chat, "Bob");
const charlie = new ChatUser(chat, "Charlie");
chat.registerUser(alice);
chat.registerUser(bob);
chat.registerUser(charlie);

alice.send("Hello everyone");
// [Bob] received: Hello everyone
// [Charlie] received: Hello everyone

// Without Mediator: each user needs reference to all others
// With Mediator: they only know the mediator

Lessons:

  • Mediator centralizes communication: components do not know each other
  • Adding new user does not require changing existing ones
  • The mediator can filter, transform or log messages
  • Reduces coupling from N*(N-1) to N*1 (each only knows the mediator)
  • CQRS uses mediator: commands and queries go through a bus
  • Event bus is a form of mediator with pub/sub

### Mediator vs Observer: which do I use?

Mediator centralizes: components talk to the mediator and it redirects. Observer decentralizes: the subject notifies observers directly. Use Mediator when interaction logic is complex and you want to centralize it (chat room, wizard form). Use Observer when you just need to notify changes (data binding, event handling). Mediator knows the components; Observer does not.






End of document. Review and update quarterly.

## Common Production Pitfalls

- Applying the pattern where no abstraction is needed, adding accidental complexity.
- Letting the pattern leak into unrelated modules and blur ownership boundaries.
- Over-engineering the first implementation instead of starting simple and measuring pain.
- Skipping contract tests, so refactors silently break consumers.
- Ignoring failure modes that the pattern does not cover.
- Using the pattern as a default instead of choosing the right tool for the current scale.
- Forgetting to document when to stop using the pattern and what replaces it.
- Missing observability around the pattern's performance and error propagation.

Frequently Asked Questions

How is this different from Observer?

Observer is one-to-many broadcast. Mediator is many-to-many routing through a central coordinator.

When should I use a state manager instead?

When the primary need is shared state, not just communication. Mediator handles messages; state managers handle data.