Back to Blog
design-patternscsharptypescriptbehavioral-patterns

Mediator Pattern — FixIt Pro Series #16

Homeowners, Handymen, and the Scheduler all need to talk to each other, but none of them should know about each other directly. Learn how the Mediator pattern centralises communication through a DispatchCenter in C# and TypeScript.

Series: Design Patterns with FixIt Pro  ·  Episode 16 / 22  ·  Behavioral Pattern
Previous: #15 — Iterator Pattern


The Scenario

FixIt Pro has several actors that need to communicate:

  • Homeowner — submits job requests, receives status updates
  • Handyman — receives job assignments, sends completion signals
  • Scheduler — books time slots, notifies relevant parties
  • BillingService — generates invoices when a job completes

Without a Mediator, each component holds references to the others it needs to talk to:

Homeowner → Scheduler
Homeowner → BillingService
Handyman  → Homeowner
Handyman  → BillingService
Scheduler → Handyman
Scheduler → Homeowner

Six direct connections for four components. Add a fifth, say, a SupervisorNotifier, and you add four more connections. The web grows into a tightly coupled mesh. Change one component and you ripple through all the others it references directly.

The Mediator collapses all those direct connections into one: every component talks to the mediator, and the mediator routes.


What Is the Mediator Pattern?

Define an object that encapsulates how a set of objects interact. The Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.

Instead of N × (N-1) direct connections, you get N connections to one mediator. Components become colleagues, they know about the mediator but not about each other.

The three participants

Role FixIt Pro equivalent
Mediator interface IDispatchMediator — declares Notify(sender, event, data)
Concrete Mediator DispatchCenter — routes events between colleagues
Colleagues Homeowner, Handyman, Scheduler, BillingService

C# Implementation

// ── Mediator Interface ─────────────────────────────────────

public interface IDispatchMediator
{
    void Notify(object sender, string eventName, object? data = null);
}

// ── Base Colleague ─────────────────────────────────────────

public abstract class Colleague
{
    protected IDispatchMediator Mediator { get; }
    public    string            Name     { get; }

    protected Colleague(IDispatchMediator mediator, string name)
    {
        Mediator = mediator;
        Name     = name;
    }
}

// ── Concrete Colleagues ────────────────────────────────────

public class Homeowner : Colleague
{
    public Homeowner(IDispatchMediator mediator, string name)
        : base(mediator, name) { }

    // Homeowner submits a job request
    public void SubmitJobRequest(string jobTitle, string category)
    {
        Console.WriteLine($"\n[{Name}] Submitting job request: '{jobTitle}' [{category}]");

        Mediator.Notify(this, "JobRequested", new
        {
            Title    = jobTitle,
            Category = category,
            Client   = Name
        });
    }

    // Homeowner receives a status update from the mediator
    public void ReceiveUpdate(string message)
    {
        Console.WriteLine($"  [{Name}] Received update: {message}");
    }
}

public class Handyman : Colleague
{
    public string Specialisation { get; }

    public Handyman(IDispatchMediator mediator, string name, string specialisation)
        : base(mediator, name)
    {
        Specialisation = specialisation;
    }

    // Handyman marks a job complete
    public void CompleteJob(string jobId)
    {
        Console.WriteLine($"\n[{Name}] Marking Job #{jobId} as complete.");
        Mediator.Notify(this, "JobCompleted", new { JobId = jobId, Handyman = Name });
    }

    // Handyman receives an assignment from the mediator
    public void ReceiveAssignment(string jobTitle, string slot)
    {
        Console.WriteLine($"  [{Name}] Assigned: '{jobTitle}' at {slot}");
    }
}

public class Scheduler : Colleague
{
    private int _slotCounter = 1;

    public Scheduler(IDispatchMediator mediator)
        : base(mediator, "Scheduler") { }

    public string BookSlot(string category)
    {
        var slot = $"Slot-{_slotCounter++} ({category})";
        Console.WriteLine($"  [Scheduler] Booked: {slot}");
        return slot;
    }
}

public class BillingService : Colleague
{
    public BillingService(IDispatchMediator mediator)
        : base(mediator, "BillingService") { }

    public void GenerateInvoice(string jobId, string handyman)
    {
        Console.WriteLine($"  [BillingService] Invoice generated for Job #{jobId} " +
                          $"completed by {handyman}.");
    }
}

// ── Concrete Mediator — DispatchCenter ─────────────────────

public class DispatchCenter : IDispatchMediator
{
    // The mediator holds references to all colleagues
    private Homeowner?      _homeowner;
    private Handyman?       _handyman;
    private Scheduler?      _scheduler;
    private BillingService? _billing;

    // Register colleagues
    public void Register(Homeowner      homeowner) => _homeowner = homeowner;
    public void Register(Handyman       handyman)  => _handyman  = handyman;
    public void Register(Scheduler      scheduler) => _scheduler = scheduler;
    public void Register(BillingService billing)   => _billing   = billing;

    // Central routing logic
    public void Notify(object sender, string eventName, object? data = null)
    {
        Console.WriteLine($"  [DispatchCenter] Event: '{eventName}' from {GetName(sender)}");

        switch (eventName)
        {
            case "JobRequested":
                // Scheduler books a slot
                // Handyman gets assigned
                // Homeowner gets confirmation
                dynamic d = data!;
                var slot = _scheduler!.BookSlot((string)d.Category);
                _handyman!.ReceiveAssignment((string)d.Title, slot);
                _homeowner!.ReceiveUpdate(
                    $"Your job '{d.Title}' is booked for {slot} " +
                    $"and assigned to {_handyman.Name}.");
                break;

            case "JobCompleted":
                // Billing generates invoice
                // Homeowner gets notified
                dynamic c = data!;
                _billing!.GenerateInvoice((string)c.JobId, (string)c.Handyman);
                _homeowner!.ReceiveUpdate(
                    $"Job #{c.JobId} has been completed by {c.Handyman}. " +
                    $"Invoice is on its way.");
                break;
        }
    }

    private static string GetName(object sender) => sender switch
    {
        Colleague c => c.Name,
        _           => sender.GetType().Name
    };
}

// ── Client Code ────────────────────────────────────────────

class Program
{
    static void Main()
    {
        // Create the mediator
        var center = new DispatchCenter();

        // Create colleagues — each only knows the mediator
        var homeowner = new Homeowner(center,   "Sarah Ndapewa");
        var handyman  = new Handyman(center,    "James Nghipandua", "Plumbing");
        var scheduler = new Scheduler(center);
        var billing   = new BillingService(center);

        // Register with mediator
        center.Register(homeowner);
        center.Register(handyman);
        center.Register(scheduler);
        center.Register(billing);

        // Sarah submits a job — mediator coordinates everything
        homeowner.SubmitJobRequest("Replace burst pipe under sink", "Plumbing");

        // James completes the job — mediator notifies billing and homeowner
        handyman.CompleteJob("JC-501");
    }
}

Output:

[Sarah Ndapewa] Submitting job request: 'Replace burst pipe under sink' [Plumbing]
  [DispatchCenter] Event: 'JobRequested' from Sarah Ndapewa
  [Scheduler] Booked: Slot-1 (Plumbing)
  [James Nghipandua] Assigned: 'Replace burst pipe under sink' at Slot-1 (Plumbing)
  [Sarah Ndapewa] Received update: Your job 'Replace burst pipe under sink' is booked
    for Slot-1 (Plumbing) and assigned to James Nghipandua.

[James Nghipandua] Marking Job #JC-501 as complete.
  [DispatchCenter] Event: 'JobCompleted' from James Nghipandua
  [BillingService] Invoice generated for Job #JC-501 completed by James Nghipandua.
  [Sarah Ndapewa] Received update: Job #JC-501 has been completed by James Nghipandua.
    Invoice is on its way.

TypeScript Implementation

// ── Mediator/IDispatchMediator.ts ─────────────────────────

export interface IDispatchMediator {
  notify(sender: object, eventName: string, data?: Record<string, unknown>): void;
}

// ── Colleague/Colleague.ts ─────────────────────────────────

import { IDispatchMediator } from "../Mediator/IDispatchMediator";

export abstract class Colleague {
  constructor(
    protected readonly mediator: IDispatchMediator,
    readonly name: string
  ) {}
}

// ── Colleague/Homeowner.ts ─────────────────────────────────

import { Colleague         } from "./Colleague";
import { IDispatchMediator } from "../Mediator/IDispatchMediator";

export class Homeowner extends Colleague {
  constructor(mediator: IDispatchMediator, name: string) {
    super(mediator, name);
  }

  submitJobRequest(jobTitle: string, category: string): void {
    console.log(`\n[${this.name}] Submitting job request: '${jobTitle}' [${category}]`);

    this.mediator.notify(this, "JobRequested", {
      title:    jobTitle,
      category: category,
      client:   this.name,
    });
  }

  receiveUpdate(message: string): void {
    console.log(`  [${this.name}] Received update: ${message}`);
  }
}

// ── Colleague/Handyman.ts ──────────────────────────────────

import { Colleague         } from "./Colleague";
import { IDispatchMediator } from "../Mediator/IDispatchMediator";

export class Handyman extends Colleague {
  constructor(
    mediator: IDispatchMediator,
    name: string,
    readonly specialisation: string
  ) {
    super(mediator, name);
  }

  completeJob(jobId: string): void {
    console.log(`\n[${this.name}] Marking Job #${jobId} as complete.`);
    this.mediator.notify(this, "JobCompleted", { jobId, handyman: this.name });
  }

  receiveAssignment(jobTitle: string, slot: string): void {
    console.log(`  [${this.name}] Assigned: '${jobTitle}' at ${slot}`);
  }
}

// ── Colleague/Scheduler.ts ─────────────────────────────────

import { Colleague         } from "./Colleague";
import { IDispatchMediator } from "../Mediator/IDispatchMediator";

export class Scheduler extends Colleague {
  private slotCounter = 1;

  constructor(mediator: IDispatchMediator) {
    super(mediator, "Scheduler");
  }

  bookSlot(category: string): string {
    const slot = `Slot-${this.slotCounter++} (${category})`;
    console.log(`  [Scheduler] Booked: ${slot}`);
    return slot;
  }
}

// ── Colleague/BillingService.ts ────────────────────────────

import { Colleague         } from "./Colleague";
import { IDispatchMediator } from "../Mediator/IDispatchMediator";

export class BillingService extends Colleague {
  constructor(mediator: IDispatchMediator) {
    super(mediator, "BillingService");
  }

  generateInvoice(jobId: string, handyman: string): void {
    console.log(
      `  [BillingService] Invoice generated for Job #${jobId} completed by ${handyman}.`
    );
  }
}

// ── Mediator/DispatchCenter.ts ─────────────────────────────

import { IDispatchMediator } from "./IDispatchMediator";
import { Homeowner         } from "../Colleague/Homeowner";
import { Handyman          } from "../Colleague/Handyman";
import { Scheduler         } from "../Colleague/Scheduler";
import { BillingService    } from "../Colleague/BillingService";
import { Colleague         } from "../Colleague/Colleague";

export class DispatchCenter implements IDispatchMediator {
  private homeowner?: Homeowner;
  private handyman?:  Handyman;
  private scheduler?: Scheduler;
  private billing?:   BillingService;

  registerHomeowner(c: Homeowner):      void { this.homeowner = c; }
  registerHandyman(c: Handyman):        void { this.handyman  = c; }
  registerScheduler(c: Scheduler):      void { this.scheduler = c; }
  registerBilling(c: BillingService):   void { this.billing   = c; }

  notify(
    sender:    object,
    eventName: string,
    data:      Record<string, unknown> = {}
  ): void {
    const senderName = sender instanceof Colleague ? sender.name : "Unknown";
    console.log(`  [DispatchCenter] Event: '${eventName}' from ${senderName}`);

    switch (eventName) {
      case "JobRequested": {
        const { title, category } = data as { title: string; category: string };
        const slot = this.scheduler!.bookSlot(category);
        this.handyman!.receiveAssignment(title, slot);
        this.homeowner!.receiveUpdate(
          `Your job '${title}' is booked for ${slot} and assigned to ${this.handyman!.name}.`
        );
        break;
      }

      case "JobCompleted": {
        const { jobId, handyman } = data as { jobId: string; handyman: string };
        this.billing!.generateInvoice(jobId, handyman);
        this.homeowner!.receiveUpdate(
          `Job #${jobId} has been completed by ${handyman}. Invoice is on its way.`
        );
        break;
      }
    }
  }
}

// ── App.ts ─────────────────────────────────────────────────

import { DispatchCenter } from "./Mediator/DispatchCenter";
import { Homeowner      } from "./Colleague/Homeowner";
import { Handyman       } from "./Colleague/Handyman";
import { Scheduler      } from "./Colleague/Scheduler";
import { BillingService } from "./Colleague/BillingService";

const center = new DispatchCenter();

const homeowner = new Homeowner(center,   "Sarah Ndapewa");
const handyman  = new Handyman(center,    "James Nghipandua", "Plumbing");
const scheduler = new Scheduler(center);
const billing   = new BillingService(center);

center.registerHomeowner(homeowner);
center.registerHandyman(handyman);
center.registerScheduler(scheduler);
center.registerBilling(billing);

homeowner.submitJobRequest("Replace burst pipe under sink", "Plumbing");
handyman.completeJob("JC-501");

C# vs TypeScript — Key Differences

Aspect C# TypeScript
Dynamic data payload dynamic keyword for anonymous types Record<string, unknown> with type assertion
Switch routing switch (eventName) + dynamic cast switch (eventName) + destructured cast
Overloaded Register Register(Homeowner), Register(Handyman) etc. Separate named methods — no overloading on type
Pattern matching sender switch { Colleague c => c.Name, _ => ... } sender instanceof Colleague ? sender.name : ...
Null safety Nullable reference types Colleague? Optional chaining this.homeowner?.receiveUpdate()

C#'s method overloading on Register is clean, same method name, different parameter types. TypeScript doesn't support true method overloading, so separate named methods (registerHomeowner, registerHandyman) are the idiomatic alternative.


Mediator vs Facade — What's the Difference?

Both centralise access to multiple components. The difference is direction:

Mediator Facade
Direction Multi-directional — colleagues communicate through it One-directional — client calls it, it calls subsystems
Colleagues know mediator Yes — they hold a reference and call Notify No — subsystems don't know the Facade exists
Who initiates Any colleague can send an event Only external clients
Coupling reduced Between colleagues Between client and subsystems
FixIt Pro DispatchCenter routes between Homeowner, Handyman, Scheduler, Billing JobDispatchFacade hides scheduling complexity from callers

The Facade is a simplification layer. The Mediator is a communication hub.


When to Use the Mediator

Use it when:

  • A set of objects communicate in complex, well-defined but tangled ways
  • Reusing a component is difficult because it holds references to many others
  • You find yourself creating many subclasses just to customise communication behaviour

Avoid it when:

  • The mediator becomes a "God Object", knowing too much and doing too much
  • The communication between components is simple, direct references are cleaner
  • Only two or three objects need to interact, the pattern adds indirection without benefit

Real-World Takeaway

The Mediator is the pattern behind air traffic control, aircraft don't communicate directly with each other, they communicate through the tower. In software, it's behind event buses (MediatR in .NET, EventEmitter in Node.js, NgRx in Angular). MediatR is the most widely used Mediator implementation in the .NET ecosystem, if you've used CQRS with IRequest and IRequestHandler, you've used this pattern. In frontend, Redux's store acts as a mediator between components that dispatch actions and reducers that handle them.

In FixIt Pro, Sarah submits a job. She doesn't know about James, the Scheduler, or the BillingService. James completes a job. He doesn't know about Sarah or billing. The DispatchCenter knows about everyone, and coordinates everything. Add a SupervisorNotifier colleague tomorrow and only the mediator changes.


Repo Structure for This Episode

github.com/antonlungameni/fixit-pro-design-patterns

fixit-pro-design-patterns/
├── csharp/Behavioral/16-Mediator/
│   ├── Mediator/IDispatchMediator.cs
│   ├── Mediator/DispatchCenter.cs
│   ├── Colleague/Colleague.cs
│   ├── Colleague/Homeowner.cs
│   ├── Colleague/Handyman.cs
│   ├── Colleague/Scheduler.cs
│   ├── Colleague/BillingService.cs
│   └── Program.cs
└── typescript/Behavioral/16-Mediator/
    ├── Mediator/IDispatchMediator.ts
    ├── Mediator/DispatchCenter.ts
    ├── Colleague/Colleague.ts
    ├── Colleague/Homeowner.ts
    ├── Colleague/Handyman.ts
    ├── Colleague/Scheduler.ts
    ├── Colleague/BillingService.ts
    └── App.ts

Previous: #15 — Iterator Pattern
Next up: #17 — Memento Pattern
Saving and restoring job card state — undo a status change from "Completed" back to "In Progress" without exposing internal state to the outside world.