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.