Wire 10 objects to talk directly and you've signed up for 45 connections, each one a place where coordination logic can hide. Mediator is the Gang of Four behavioral pattern whose stated essence is to define an object that encapsulates how a set of objects interact, so the objects stop referencing each other and talk only to the hub.
A mediator is really just a group chat for your objects: everyone sends to the room, and the room decides who hears what. The decisive word is decides. A mediator owns interaction rules, who can talk, in what order, under what conditions, which is what separates it from plain message forwarding and gives the pattern both its value and its best-known failure mode.
Here's the direct-reference version of a three-user chat. Every user keeps a peer list, connecting users is a mutual handshake, and the arithmetic in the comments is the quadratic formula n * (n - 1) / 2 doing its quiet work:
Three users need 3 handshakes, ten need 45, and fifty need 1,225. The count is only half the problem. The closing comment asks where muting goes, and the honest answer is everywhere: with no shared place for interaction rules, every behavior that concerns the group, muting, rate limits, message ordering, gets smeared across every peer class, implemented slightly differently in each.
The mediator version introduces a ChatRoom that owns the roster and the rules. Users hold one reference, the room, and their send() is a single delegation. The room's broadcast() carries the coordination logic, including a mute list. Before reading past the code, trace the two print statements: alice sends hi, then bob is muted and sends yo:
The first line prints 1 1, since alice's message reached bob and carol but not herself. The second prints 0 1: bob's message reached nobody, because the room checked the mute list and dropped it, and alice's inbox was always empty since senders don't receive their own messages. The part worth noticing is what didn't change. Muting shipped without touching the User class, because the rule lives where all rules now live, in the one object whose job is rules.
Three roles. The Mediator interface declares the communication surface colleagues use. The ConcreteMediator (ChatRoom) implements it, knows every colleague, and owns the interaction logic. Colleagues (User) hold a mediator reference, report events to it, and react when it calls them, knowing nothing of each other. The mesh's many-to-many becomes hub-and-spoke, with the complexity relocated rather than deleted, sitting now in one inspectable place.
The pattern works like air traffic control: pilots never negotiate with each other about who lands first, they talk to the tower, and the tower sequences everyone. The analogy carries an honest warning in both directions. A tower is a single point whose congestion or failure affects every plane, and a mediator concentrates risk the same way. And unlike aviation, code has no regulation forcing traffic through the tower: a colleague can always sneak a direct reference to another, and only review discipline stops the mesh from growing back underneath the hub.
Installing the pattern is four moves:
ChatRoom, not ObjectCoordinator) and a small set of verbs colleagues can say to it, like broadcast and join. The vocabulary is the design.User holds one reference, the room, and zero references to other users. The quadratic mesh collapses to n spokes the moment nobody knows anybody.broadcast(), not in any user, which is what separates a mediator from a dumb dispatcher. The hub holds the interaction logic, and that concentration is both the pattern's power and its famous failure mode.join() call, with zero edits to alice, bob, or carol. Colleague count becomes the mediator's private bookkeeping instead of everyone's wiring problem.Three patterns get tangled here, and two questions sort them. Does the middle object hold interaction logic? An observer's subject broadcasts to subscribers with no opinions about them, while a mediator decides who hears what, the way the room enforced muting, which is the smart-hub versus dumb-fan-out split the pattern literature draws. And does communication flow both ways? A Facade fronts a subsystem that never knows the facade exists, one-directional by design, while colleagues know their mediator and converse with it constantly.
You might also think React's Context is this pattern, since both sit in the middle of many components. React's own docs describe Context as a way to pass data deeply without prop threading, a vessel holding a value, with no coordination logic anywhere in it, which fails the smart-hub test. Context is closer to a broadcast channel. The verdict flips only when what you pass through Context is itself a dispatcher that routes between components, at which point you've built a mediator and used Context as its delivery truck.
Go's most natural mediator doesn't look like the pattern diagram at all: it's a hub goroutine selecting over channels, with each client holding a send channel and the hub owning the routing loop. Every Go chat-server tutorial builds exactly this shape, a mediator spelled in concurrency primitives instead of method calls. The code tab's struct version is the synchronous equivalent for when goroutines would be ceremony.
Rust pushes the design further in the same direction the borrow checker always pushes: bidirectional object references are painful on purpose, so the idiomatic mediator simply owns its colleagues. The code tab's ChatRoom holds the Vec<User> outright and hands out indices instead of references, and the mesh version above it shows the alternative, Rc<RefCell> spreading through every signature. When a Rust design fights shared mutable references, the mediator's own-everything shape is frequently the answer the compiler was steering toward.
Mediator's failure mode is so reliable that the standard reference lists it as the pattern's main con: over time a mediator can evolve into a god object. The mechanism is gravitational. The hub already knows everyone, so every new cross-cutting behavior is easiest to put there, and each addition makes the next one look more at home, until the 45 connections you removed from the mesh have reassembled inside one class as a thicket of conditionals that touches everything and tests like wet cement.
The countermeasures are in the best practices below, and they share one principle: the mediator earns its existence by routing and sequencing, not by knowing things. The moment it computes something a colleague could compute, the boundary has slipped, and god objects are built one slipped boundary at a time.
The hub concentrates power, and these five keep it accountable:
Two questions before you go. First: muting bob required zero changes to the User class. Where does the rule live, and what about the pattern's structure made that the only place it could go? Second: Observer also decouples a sender from its receivers, so what does a mediator have that a broadcasting subject doesn't, and why is that same thing the seed of the god-object problem? Both answers are on this page.
Then go spot it in the wild. Chat servers are the textbook case, every client talking to a hub that routes. Air traffic control runs the physical version, and UI dialogs where the checkbox disables the field that clears the button are the GoF's original motivating tangle, begging for one coordinator. Last, grep your codebase for two classes that import each other. That circular pair is a two-node mesh, and the day a third node joins their conversation is the day this pattern starts paying rent.
class User:
def __init__(self, name: str):
self.name = name
self.inbox: list[str] = []
self.peers: list["User"] = [] # every user tracks every other user
def connect(self, other: "User") -> None:
# Wiring grows quadratically: n users need n*(n-1)/2 of these
self.peers.append(other)
other.peers.append(self)
def send(self, message: str) -> None:
for peer in self.peers:
peer.receive(self.name, message)
def receive(self, sender: str, message: str) -> None:
self.inbox.append(sender + ": " + message)
# Example usage: 3 users already need 3 connections; 10 need 45
alice = User("alice")
bob = User("bob")
carol = User("carol")
alice.connect(bob)
alice.connect(carol)
bob.connect(carol)
alice.send("hi")
print(len(bob.inbox)) # 1
# Now add muting. Which of the three classes does it go in?
# All of them. That's the problem.class User: # Colleague: knows the room, never the roster
def __init__(self, name: str, room: "ChatRoom"):
self.name = name
self.inbox: list[str] = []
self.room = room
room.join(self)
def send(self, message: str) -> None:
self.room.broadcast(self.name, message)
def receive(self, sender: str, message: str) -> None:
self.inbox.append(sender + ": " + message)
class ChatRoom: # Mediator: owns the roster AND the rules
def __init__(self):
self._users: list[User] = []
self._muted: set[str] = set()
def join(self, user: User) -> None:
self._users.append(user)
def mute(self, name: str) -> None:
self._muted.add(name)
def broadcast(self, sender: str, message: str) -> None:
# Coordination logic lives here, in exactly one place
if sender in self._muted:
return
for user in self._users:
if user.name != sender:
user.receive(sender, message)
# Example usage: users never reference each other
room = ChatRoom()
alice = User("alice", room)
bob = User("bob", room)
carol = User("carol", room)
alice.send("hi")
print(len(bob.inbox), len(carol.inbox)) # 1 1
room.mute("bob")
bob.send("yo")
print(len(alice.inbox), len(carol.inbox)) # 0 1