intermediate~2h

Mediator Pattern

Learn how introducing one central coordinator collapses a tangled web of objects that all reference each other directly into a simple hub-and-spoke design, using a group chat as the running example.

Learning objectives

  • Recognize when objects referencing each other directly creates an unmanageable web of dependencies
  • Introduce a mediator object that every participant talks to instead of talking to each other
  • Distinguish sending from receiving in a mediator-coordinated broadcast
  • Understand that Mediator concentrates coupling rather than eliminating it, and when that trade is worth making

◆ Story

Planes approaching a busy airport do not coordinate landing order by talking directly to every other plane in the sky — that would require every pilot to track every other plane's position, speed, and intentions at once. Instead, every plane talks only to one air traffic controller, who holds the whole picture and tells each plane what to do. No plane ever needs to know about any other plane directly.

That is the shape of problem the Mediator pattern solves: instead of every participant in a system knowing about, and talking to, every other participant, each one talks only to a single coordinator, which is the only object that ever needs the full picture.

A group chat needs exactly this. Any user in a chat room can send a message, and every other user in the room should receive it — think of a WhatsApp group. The obvious first instinct is to have each user hold a direct reference to every other user and call a method on each of them to deliver a message. That works for a handful of people. It is worth building that version first, and watching precisely how badly it scales once the group grows.

The obvious way to let users message each other is to give each User a sendMessage() method that takes the message and the specific recipient, and have the sender call it once per person they want to reach.

For three users, this is perfectly fine code. Rahul can message Amit, then message Neha, with two explicit calls. It runs and prints exactly what you would expect.

The trouble is what this requires from the caller. Broadcasting one message to a group of N people means N minus 1 explicit sendMessage() calls, written out by whoever is sending. Rahul has to know Amit's and Neha's identities directly to reach them at all — there is no way to say "send this to the room," only "send this to this specific person," repeated by hand for every recipient.

This gets worse as the group grows. The number of potential direct connections between users grows roughly with the square of the group size — adding one new person means every existing sender potentially needs new code to reach them. And there is nowhere for shared behavior to live: if the app later needs to log every message, or block a user from sending, there is no single place to add that logic — it would have to be duplicated into every user's own sending code.

💻 Code example

class User { private final String name; User(String name) { this.name = name; } String getName() { return name; } void sendMessage(String message, User recipient) { System.out.println(name + " is sending message to " + recipient.getName() + ": " + message); } } // Broadcasting "hello" to a group means calling sendMessage() once per // other user -- and the sender has to know each recipient directly. User rahul = new User("Rahul"); User amit = new User("Amit"); User neha = new User("Neha"); rahul.sendMessage("Hello", amit); // Rahul has to know about Amit, directly rahul.sendMessage("Hello", neha); // and about Neha, directly, too // Works for 3 users. What happens when the group has 50 people, and // Neha wants to broadcast too?

A user should not need to know who else is in the room — only that when they send a message, it reaches everyone who should get it. That means something else needs to hold the actual list of participants, and be responsible for distributing a message to all of them. Every user then talks only to that one thing, never to each other directly, and that one thing becomes the only place in the system that ever needs to know the full participant list.

That is the Mediator pattern: introduce one central object that every participant talks to instead of talking to each other directly, collapsing a web of many-to-many connections down to each participant having exactly one connection — to the mediator.

The shared contract, ChatMediator, declares two operations: joining the room, and broadcasting a message on behalf of a sender. ChatRoom, the concrete mediator, is the only class in the whole system that holds the actual list of users, and its sendMessage() loops over that list, skipping the original sender so nobody receives an echo of their own message.

ChatUser changes shape to match: it holds exactly one reference to anything else in the system — the mediator — and nothing to any other user. Its own sendMessage() just hands the message to the mediator; its new receiveMessage() method is called only by the mediator, never by another user directly. One participant sends, everyone else receives, and the mediator is what turns one into the other.

💻 Code example

interface ChatMediator { void addUser(ChatUser user); // join the room void sendMessage(String message, ChatUser sender); // broadcast, on the sender's behalf } class ChatRoom implements ChatMediator { private final List<ChatUser> users = new ArrayList<>(); // the ONLY place the full list lives public void addUser(ChatUser user) { users.add(user); } public void sendMessage(String message, ChatUser sender) { for (ChatUser user : users) { if (user != sender) { // don't echo the message back to the sender user.receiveMessage(message, sender); } } } } class ChatUser { private final String name; private final ChatMediator mediator; // the ONLY other object this class knows about ChatUser(String name, ChatMediator mediator) { this.name = name; this.mediator = mediator; } void sendMessage(String message) { System.out.println(name + " is sending message: " + message); mediator.sendMessage(message, this); // hand it to the mediator -- no idea who else is in the room } void receiveMessage(String message, ChatUser sender) { // called BY the mediator only System.out.println(name + " received message from " + sender.getName() + ": " + message); } String getName() { return name; } }

Wiring up three users and broadcasting confirms the design behaves correctly: when Amit sends "Hi everyone," Rahul and Neha both receive it, and Amit himself does not — thanks to the sender check inside ChatRoom.sendMessage().

Now add a fourth participant, Priya, and see exactly what changes. In the naive version, every existing user potentially needed new code to reach a new participant. Here, exactly one new ChatUser object is created and handed to chatRoom.addUser() — Rahul, Amit, and Neha's own code is not touched at all. The very next broadcast from any existing user automatically reaches Priya too, because the mediator, not the individual users, is what holds the participant list.

That is the concrete payoff: adding a participant to a growing system is now a one-line addition at the mediator, rather than a change rippling out across every existing object that might want to reach the new one.

💻 Code example

public class Main { public static void main(String[] args) { ChatMediator chatRoom = new ChatRoom(); ChatUser rahul = new ChatUser("Rahul", chatRoom); ChatUser amit = new ChatUser("Amit", chatRoom); ChatUser neha = new ChatUser("Neha", chatRoom); chatRoom.addUser(rahul); chatRoom.addUser(amit); chatRoom.addUser(neha); amit.sendMessage("Hi everyone"); // "Amit is sending message: Hi everyone" // "Rahul received message from Amit: Hi everyone" // "Neha received message from Amit: Hi everyone" // (Amit does NOT receive his own message, thanks to the != sender check) // Adding a fourth participant -- the entire diff: ChatUser priya = new ChatUser("Priya", chatRoom); // one new object chatRoom.addUser(priya); amit.sendMessage("Welcome Priya!"); // Rahul, Neha, AND Priya all receive it // Rahul, Amit, Neha's own code was never touched. } }

▲ Common mistake: forgetting the "don't message yourself" check

Without the if (user != sender) guard inside ChatRoom.sendMessage(), the sender would receive an echo of their own broadcast message back. This is a genuinely easy corner case to miss on a first pass, and worth explicitly testing for whenever building a broadcast mechanism like this one.

▲ Edge case: a growing mediator is a sign to split responsibilities, not proof the pattern failed

Mediator does not eliminate coupling between objects — it concentrates it. All the complexity that used to be spread thin across many participants' direct connections now sits inside one ChatRoom class, and that class can grow into an unwieldy "god object" if message routing, logging, moderation, and everything else keeps getting piled directly into it without discipline. This is a real, worthwhile trade — many small, tangled dependencies traded for one well-understood, centralized one — but it is a trade, not a free win. If ChatRoom starts accumulating message history, presence tracking, and rate limiting all by itself, the fix is not to abandon the mediator; it is to split those concerns into their own collaborating classes — a MessageHistory, a ModerationService — that the mediator coordinates rather than implements directly.

A mediator often pairs naturally with the Observer pattern: instead of calling specific methods on specific participants directly, a mediator can notify all of its participants of relevant events by treating itself as a subject and its participants as observers of it.

◆ Under the hood

The Mediator pattern shows up anywhere a group of participants needs to coordinate without each one holding a direct reference to every other.

  • Air traffic control — this chapter's opening story, at real scale: planes coordinate through a central tower rather than directly with each other.
  • GUI component coordination — a dropdown changing selection might need to update several other fields on a form; instead of each component knowing about every other component, they all interact through a central mediator that knows what needs to change.
  • Workflow and business process systems — a mediator coordinating activities across multiple departments or systems, rather than each system integrating directly with every other one it might need to talk to.
  • Chat and messaging platforms — this chapter's exact example, at real-world scale, with rooms holding thousands of participants instead of three.

The common thread is a system where the number of potential direct relationships would otherwise grow far faster than the number of participants.

Q: What problem does the Mediator pattern actually solve? : It collapses a tangled web of many objects directly referencing each other — roughly N-squared connections — down to each object only knowing one thing: the mediator.

Q: In the ChatRoom example, why does ChatUser call mediator.sendMessage() instead of calling other users directly? : So ChatUser never needs to know who else is in the room. The mediator is the only object that holds the participant list, and the only object that needs updating when participants change.

Q: Why does ChatRoom check "user != sender" before calling receiveMessage()? : To prevent the sender from receiving an echo of their own broadcast message back — an easy corner case to miss without explicitly testing for it.

Q: Does Mediator eliminate coupling, or just move it? : It concentrates coupling into one place, the mediator itself, trading many scattered dependencies for one centralized one — which needs to stay genuinely focused to avoid becoming its own tangled mess.

Q: How does Mediator commonly pair with the Observer pattern? : The mediator often acts as a subject, notifying its participants — acting as observers — of relevant events, rather than calling specific methods on specific objects directly.

Want a visual for this concept?

Generate a diagram tailored to “Mediator Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Singleton Pattern← Back to all Low-Level Design & Design Patterns chapters