Mediator Design Pattern in Java

The Mediator pattern gives interacting objects one place for their coordination rules. Follow a Java chat room that routes messages and rejects an unknown recipient.

Mediator design pattern

The Mediator pattern gives a group of objects one place to coordinate their interactions. Each participant asks that mediator to contact the right object instead of keeping references to every other participant. It is useful when the connections and shared rules between objects are becoming hard to manage.

“Define an object that encapsulates how a set of objects interact.”

Consider a small chat feature inside one Java application. A user sends a message with a recipient ID. The chat room looks up the recipient and delivers the message. We can follow that path without any network or database code getting in the way.

1. How the Mediator Pattern Works

The pattern has a coordinator and the objects that use it. Pattern descriptions often call those objects colleagues. Here, a colleague simply means a participant in the interaction.

Participant What it does
Mediator interface Declares the interactions participants can request.
Concrete mediator Knows the participants and carries out the coordination rules.
Colleagues Keep their own behavior and ask the mediator to coordinate with others.
Client Creates and connects the mediator and colleagues.

A colleague calls the mediator. The mediator decides which other colleague needs the request, then calls it. Colleagues depend on the mediator contract instead of directly depending on one another. This is the path through the generic roles:

Generic Mediator UML diagram showing colleagues communicating through a mediator interface and concrete mediator

The mediator does not remove every dependency. Colleagues still need a way to identify the requested interaction, and the coordinator needs enough knowledge to carry it out.

2. Mediator Pattern in Action

Our example is a chat feature that runs inside one process. The client creates a room and registers Alex and Brian. Registration gives the room a directory of users by ID. It is only setup for this example; it does not authenticate the users. Alex then asks the room to send to Brian’s ID; the room finds Brian and calls his receive() method.

Pattern participant Demo class or interface Responsibility
Mediator interface ChatMediator Declares register() and send().
Concrete mediator ChatRoom Stores registered users and routes a message by recipient ID.
Colleague ChatUser Sends through the room and displays a received message.
Client MediatorDemo Connects the objects and exercises the feature.

A user holds a reference to ChatMediator, not a reference to another user. The class diagram shows that relationship alongside the room and its recipient lookup. The room holds a directory of registered users. When a recipient ID is unknown, the room rejects the request rather than pretending delivery succeeded.

Concrete Mediator UML diagram showing MediatorDemo, ChatMediator, ChatRoom and ChatUser relationships

3. Mediator Pattern Implementation

We can keep the routing code small enough to follow one request at a time. The complete runnable source on GitHub includes the package declaration and imports omitted from these excerpts.

3.1. Define the Mediator Contract

The ChatMediator interface is the shared agreement between users and the room. It declares two operations: register() adds a user to the room, and send() sends a message to a registered recipient. The interface does not expose how the room stores users.

interface ChatMediator {
    void register(ChatUser user);
    void send(String recipientId, String message);
}

3.2. Register and Find Users

ChatRoom uses a map to find a user quickly from an ID. Each key is a user ID, and each value is a ChatUser. Its register() method rejects a null user and a duplicate ID. The map never stores a null user, so a non-null result from putIfAbsent() means that ID was already registered. In send(), an absent lookup produces a clear exception. A valid lookup calls the recipient’s receive() method immediately.

final class ChatRoom implements ChatMediator {
    private final Map<String, ChatUser> users = new HashMap<>();

    @Override
    public void register(ChatUser user) {
        Objects.requireNonNull(user, "user");
        if (users.putIfAbsent(user.id(), user) != null) {
            throw new IllegalArgumentException("User is already registered: " + user.id());
        }
    }

    @Override
    public void send(String recipientId, String message) {
        Objects.requireNonNull(recipientId, "recipientId");
        Objects.requireNonNull(message, "message");
        ChatUser recipient = users.get(recipientId);
        if (recipient == null) {
            throw new IllegalArgumentException("Unknown recipient: " + recipientId);
        }
        recipient.receive(message);
    }
}

A ChatUser knows its own ID and name and holds the ChatMediator it uses. Its send() method adds the sender’s name and passes the message to the room. Its receive() method prints what arrived. A user does not need to look up another user directly.

final class ChatUser {
    private final String id;
    private final String name;
    private final ChatMediator room;

    ChatUser(String id, String name, ChatMediator room) {
        this.id = Objects.requireNonNull(id, "id");
        this.name = Objects.requireNonNull(name, "name");
        this.room = Objects.requireNonNull(room, "room");
    }

    String id() { return id; }

    void send(String recipientId, String message) {
        room.send(recipientId, name + " says: " + Objects.requireNonNull(message, "message"));
    }

    void receive(String message) {
        System.out.println(name + " received: " + message);
    }
}

This example has one kind of user, so it does not need an abstract colleague class just to match a diagram. A larger application can introduce additional participant types when they have distinct behavior.

4. Demo

MediatorDemo creates the room and registers both users before sending. The final send uses an ID that was never registered. The demo catches that expected exception so we can see its message alongside the successful sends.

public class MediatorDemo {
    public static void main(String[] args) {
        ChatMediator room = new ChatRoom();
        ChatUser alex = new ChatUser("1", "Alex", room);
        ChatUser brian = new ChatUser("2", "Brian", room);
        room.register(alex);
        room.register(brian);

        alex.send("2", "Hello");
        brian.send("1", "Hi Alex");
        try {
            alex.send("9", "Are you there?");
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}

After cloning the GitHub repository, run these commands from its root folder.

javac -d target/mediator-demo src/main/java/com/howtodoinjava/patterns/behavioral/mediator/MediatorDemo.java
java -cp target/mediator-demo com.howtodoinjava.patterns.behavioral.mediator.MediatorDemo
Brian received: Alex says: Hello
Alex received: Brian says: Hi Alex
Unknown recipient: 9

The first two lines show delivery in each direction. The third line shows the rejected ID. A successful call here only means an in-process method ran; it does not mean a network message was delivered or saved.

5. Similar Patterns

Pattern Main job Deciding difference
Mediator Coordinates interactions between participants. Participants use the mediator for shared interaction rules.
Facade Gives a caller a simpler entry point to a subsystem. Its focus is a simpler interface for callers, not coordination among colleagues.
Observer Notifies subscribed listeners about an event. It broadcasts a change to subscribers instead of choosing a recipient using a coordination rule.

A fixed pair of objects with one simple call may be clearer with a direct reference. Mediator adds an interface and a coordinating object. It becomes valuable when several objects share interaction rules that would otherwise be repeated or scattered among them.

6. FAQs

6.1. Is This Chat Room a Message Broker?

No. ChatRoom is an in-memory object. A call to send() invokes receive() synchronously in the same process. It does not authenticate users, persist messages, retry failed delivery or communicate across a network. A real chat system needs separate components and policies for those concerns.

6.2. Can a Mediator Become Too Large?

Yes. Moving every rule into one mediator can create a class with too many reasons to change. Keep coordination for one cohesive set of interactions together, and move unrelated storage or business features to their own components.

6.3. Is This Example Safe for Concurrent Sends?

The demo runs on one thread. If several threads register users or send messages at the same time, they can interfere with the room’s shared directory. ChatRoom uses a HashMap without synchronization, so that case needs a deliberate concurrency design. Replacing the map alone does not establish what should happen when a user is registered or removed during a send.

7. Conclusion

Mediator gives a group of objects one place for their interaction rules. In the chat example, users send through a room that owns recipient lookup, while each user still handles its own received message. Use the pattern when that coordination is substantial enough to justify a separate object.

Happy Learning!

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.