A deep dive guided by insights from “Learning Patterns” (Patterns.dev) by Lydia Hallie & Addy Osmani
1. What Is the Mediator (a.k.a. Middleware) Pattern?
“The mediator pattern makes it possible for components to interact with each other through a central point: the mediator.”
Instead of every component talking directly to every other component (a chaotic many‑to‑many mesh), each sends its messages to a single mediator. The mediator then routes, augments, or blocks those messages before forwarding them. Think of it as the air‑traffic controller for your codebase: pilots (objects) never argue about who lands first—they all coordinate through the tower.
2. Core Mechanics
2.1 Chatroom Example (Vanilla JS)
class ChatRoom {
logMessage(user, message) {
const time = new Date().toLocaleString();
console.log(`${time} [${user.getName()}]: ${message}`);
}
}
class User {
constructor(name, room) {
this.name = name;
this.room = room;
}
getName() { return this.name; }
send(msg) { this.room.logMessage(this, msg); }
}
const room = new ChatRoom();
const john = new User("John Doe", room);
const jane = new User("Jane Doe", room);
john.send("Hi there!");
jane.send("Hey! 👋");
All chat messages funnel through the ChatRoom
mediator, keeping users blissfully unaware of each other’s implementation details.
2.2 Express.js Middleware Chain
Express embraces the same idea for HTTP requests: every request passes through a chain of middleware functions before the final handler responds.
const app = require("express")();
// ① Add a custom header
app.use("/", (req, res, next) => {
req.headers["x-test-header"] = "1234";
next();
});
// ② Verify the header exists
app.use("/", (req, res, next) => {
console.log("Has test header:", !!req.headers["x-test-header"]);
next();
});
// ③ Final responder
app.get("/", (req, res) => {
res.send("<h1>Hello Middleware</h1>");
});
app.listen(8080);
Each next()
hands off control to the next mediator in the pipeline, allowing incremental transforms without tight coupling.
3. Why Lydia & Addy Like Mediators
- Loose coupling. Components never import each other directly.
- Single source of coordination. Easier to debug flows when all messages converge in one spot.
- Composable behavior. Middleware functions can be chained, swapped, or reused across projects.
These traits align perfectly with the authors’ broader theme: patterns that scale as your codebase grows.
4. Five Real‑Life Software Use Cases
# | Use Case | How Mediator/Middleware Helps |
---|---|---|
1 | Express.js request pipeline | Logging, authentication, compression, and more plug into the flow without altering route handlers. |
2 | Redux middleware | Intercepts every dispatched action for logging, async APIs, or routing before reducers run. |
3 | Apollo Link chain | Adds auth tokens, retries, or error handling to every GraphQL request/response. |
4 | MediatR in .NET | Centralizes command/query processing in CQRS architectures, avoiding controller bloat. |
5 | Chatroom services (e.g., Socket.io rooms) | Users broadcast to a room mediator, which relays messages to participants, preventing n×n direct connections. |
5. Pros & Cons
✅ Benefits
- Decoupling leads to more modular, testable code.
- Separation of concerns: cross‑cutting tasks (logging, auth) live in middleware, not business logic.
- Runtime composition: insert, remove, or reorder mediators without touching callers.
⚠️ Trade‑offs
- Single point of failure if the mediator itself misbehaves.
- Indirection overhead: excessive layers can make call‑stacks harder to trace.
- Potential performance hit when chains get too long.
6. When to Choose (or Skip) the Mediator Pattern
Use it when… | Skip it when… |
---|---|
Multiple objects need to communicate but shouldn’t know about each other. | Only two modules talk; introducing a mediator adds needless indirection. |
You have cross‑cutting concerns (auth, logging) across many endpoints. | Performance is ultra‑critical and the added hop cannot be justified. |
You anticipate feature‑based plugins (e.g., add new middlewares easily). | The flow is linear and unlikely to change. |
7. Key Take‑away
Centralize interactions now to avoid spaghetti later. The Mediator/Middleware pattern shines whenever decoupled components must still collaborate—no matter if it’s HTTP requests, Redux actions, or CQRS commands.
All code samples tested on Node 18. Content referenced under CC BY‑NC 4.0.
Top comments (0)