Behavioral

Mediator Design Pattern

Define an object that centralizes communication between objects

The Mediator Pattern: The Middleman Who Actually Simplifies Things

Ever been in a group chat where everyone’s talking to everyone? It gets messy fast. Now imagine a moderator who coordinates all communication. Much cleaner, right?

That’s the Mediator pattern. Instead of objects communicating directly with each other, they communicate through a mediator that coordinates the interactions.

The Problem: Objects Talking to Everyone

You’re building a chat application. Users can send messages to each other:

class User {
    private String name;
    private List<User> contacts;
    
    public User(String name) {
        this.name = name;
        this.contacts = new ArrayList<>();
    }
    
    public void addContact(User user) {
        contacts.add(user);
    }
    
    public void sendMessage(String message, User recipient) {
        System.out.println(name + " sends to " + recipient.name + ": " + message);
        recipient.receiveMessage(message, this);
    }
    
    public void receiveMessage(String message, User sender) {
        System.out.println(name + " received from " + sender.name + ": " + message);
    }
}

Usage:

User alice = new User("Alice");
User bob = new User("Bob");
User charlie = new User("Charlie");

alice.addContact(bob);
alice.addContact(charlie);
bob.addContact(alice);
bob.addContact(charlie);
charlie.addContact(alice);
charlie.addContact(bob);

alice.sendMessage("Hi Bob", bob);
bob.sendMessage("Hi Alice", alice);

Problems:

Tight coupling: Every user knows about every other user. Add a user? Update everyone’s contact list.

Complex relationships: With 10 users, you have 90 potential connections (n*(n-1)). With 100 users? 9,900 connections.

Hard to maintain: Want to add features like message history, filtering, or logging? Update every User class.

Can’t easily change communication logic: Want to add spam filtering or rate limiting? Modify every send/receive interaction.

The Mediator Solution

Create a mediator that coordinates all communication:

// Mediator interface
interface ChatMediator {
    void sendMessage(String message, User sender);
    void addUser(User user);
}

// Concrete mediator
class ChatRoom implements ChatMediator {
    private List<User> users;
    
    public ChatRoom() {
        this.users = new ArrayList<>();
    }
    
    @Override
    public void addUser(User user) {
        this.users.add(user);
    }
    
    @Override
    public void sendMessage(String message, User sender) {
        for (User user : users) {
            // Don't send to yourself
            if (user != sender) {
                user.receive(message, sender);
            }
        }
    }
}

// Colleague
class User {
    private String name;
    private ChatMediator mediator;
    
    public User(String name, ChatMediator mediator) {
        this.name = name;
        this.mediator = mediator;
    }
    
    public void send(String message) {
        System.out.println(name + " sends: " + message);
        mediator.sendMessage(message, this);
    }
    
    public void receive(String message, User sender) {
        System.out.println(name + " received from " + sender.name + ": " + message);
    }
    
    public String getName() {
        return name;
    }
}

Usage:

ChatRoom chatRoom = new ChatRoom();

User alice = new User("Alice", chatRoom);
User bob = new User("Bob", chatRoom);
User charlie = new User("Charlie", chatRoom);

chatRoom.addUser(alice);
chatRoom.addUser(bob);
chatRoom.addUser(charlie);

alice.send("Hi everyone!");
// Bob and Charlie receive the message

bob.send("Hello Alice!");
// Alice and Charlie receive the message

Now users don’t know about each other. They only know about the chat room. The chat room handles all coordination.

The Components

1. Mediator Interface

Defines communication interface:

interface Mediator {
    void notify(Component sender, String event);
}

2. Concrete Mediator

Implements coordination logic:

class ConcreteMediator implements Mediator {
    private ComponentA a;
    private ComponentB b;
    
    public void notify(Component sender, String event) {
        if (event.equals("A")) {
            // Coordinate with B
            b.doSomething();
        }
    }
}

3. Colleague Classes

Components that communicate through mediator:

class Component {
    protected Mediator mediator;
    
    public Component(Mediator mediator) {
        this.mediator = mediator;
    }
    
    public void doAction() {
        mediator.notify(this, "action");
    }
}

Real-World Example: GUI Form Validation

Complex form with interdependent fields:

// Mediator
interface FormMediator {
    void componentChanged(Component component);
}

// Colleague base class
abstract class Component {
    protected FormMediator mediator;
    protected String value;
    
    public Component(FormMediator mediator) {
        this.mediator = mediator;
    }
    
    public void setValue(String value) {
        this.value = value;
        mediator.componentChanged(this);
    }
    
    public String getValue() {
        return value;
    }
}

// Concrete colleagues
class TextBox extends Component {
    private String label;
    
    public TextBox(FormMediator mediator, String label) {
        super(mediator);
        this.label = label;
    }
    
    public String getLabel() {
        return label;
    }
}

class CheckBox extends Component {
    private String label;
    
    public CheckBox(FormMediator mediator, String label) {
        super(mediator);
        this.label = label;
    }
    
    public boolean isChecked() {
        return "true".equals(value);
    }
}

class Button extends Component {
    private boolean enabled = true;
    
    public Button(FormMediator mediator) {
        super(mediator);
    }
    
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
    
    public boolean isEnabled() {
        return enabled;
    }
}

// Concrete mediator with validation logic
class RegistrationFormMediator implements FormMediator {
    private TextBox username;
    private TextBox password;
    private TextBox confirmPassword;
    private CheckBox termsCheckbox;
    private Button submitButton;
    
    public RegistrationFormMediator() {
        username = new TextBox(this, "Username");
        password = new TextBox(this, "Password");
        confirmPassword = new TextBox(this, "Confirm Password");
        termsCheckbox = new CheckBox(this, "I agree to terms");
        submitButton = new Button(this);
    }
    
    @Override
    public void componentChanged(Component component) {
        // Validate and update form state
        validateForm();
    }
    
    private void validateForm() {
        boolean usernameValid = username.getValue() != null && 
                               username.getValue().length() >= 3;
        boolean passwordValid = password.getValue() != null && 
                               password.getValue().length() >= 8;
        boolean passwordsMatch = password.getValue() != null && 
                                password.getValue().equals(confirmPassword.getValue());
        boolean termsAccepted = termsCheckbox.isChecked();
        
        // Enable submit button only if all conditions met
        boolean formValid = usernameValid && passwordValid && 
                           passwordsMatch && termsAccepted;
        submitButton.setEnabled(formValid);
        
        System.out.println("Form valid: " + formValid);
    }
    
    // Getters for form components
    public TextBox getUsername() { return username; }
    public TextBox getPassword() { return password; }
    public TextBox getConfirmPassword() { return confirmPassword; }
    public CheckBox getTermsCheckbox() { return termsCheckbox; }
    public Button getSubmitButton() { return submitButton; }
}

Usage:

RegistrationFormMediator form = new RegistrationFormMediator();

// Initially, submit button is disabled
System.out.println("Submit enabled: " + form.getSubmitButton().isEnabled());

// Fill in form
form.getUsername().setValue("alice123");
form.getPassword().setValue("securepass123");
form.getConfirmPassword().setValue("securepass123");
form.getTermsCheckbox().setValue("true");

// Now submit button is enabled
System.out.println("Submit enabled: " + form.getSubmitButton().isEnabled());

The mediator coordinates validation logic. Components don’t need to know about each other.

Another Example: Air Traffic Control

// Mediator
interface AirTrafficControl {
    void requestLanding(Airplane airplane);
    void requestTakeoff(Airplane airplane);
    void notifyPositionChange(Airplane airplane);
}

// Colleague
class Airplane {
    private String id;
    private AirTrafficControl atc;
    private String status;
    
    public Airplane(String id, AirTrafficControl atc) {
        this.id = id;
        this.atc = atc;
        this.status = "in air";
    }
    
    public void requestLanding() {
        System.out.println(id + " requesting landing");
        atc.requestLanding(this);
    }
    
    public void requestTakeoff() {
        System.out.println(id + " requesting takeoff");
        atc.requestTakeoff(this);
    }
    
    public void land() {
        status = "landed";
        System.out.println(id + " has landed");
    }
    
    public void takeoff() {
        status = "in air";
        System.out.println(id + " has taken off");
        atc.notifyPositionChange(this);
    }
    
    public String getId() { return id; }
    public String getStatus() { return status; }
}

// Concrete mediator
class Tower implements AirTrafficControl {
    private List<Airplane> airplanes;
    private boolean runwayFree = true;
    
    public Tower() {
        this.airplanes = new ArrayList<>();
    }
    
    public void registerAirplane(Airplane airplane) {
        airplanes.add(airplane);
    }
    
    @Override
    public void requestLanding(Airplane airplane) {
        if (runwayFree) {
            System.out.println("Tower: Cleared for landing, " + airplane.getId());
            runwayFree = false;
            airplane.land();
            runwayFree = true;
        } else {
            System.out.println("Tower: Hold position, " + airplane.getId() + 
                             ", runway occupied");
        }
    }
    
    @Override
    public void requestTakeoff(Airplane airplane) {
        if (runwayFree) {
            System.out.println("Tower: Cleared for takeoff, " + airplane.getId());
            runwayFree = false;
            airplane.takeoff();
            runwayFree = true;
        } else {
            System.out.println("Tower: Hold position, " + airplane.getId() + 
                             ", runway occupied");
        }
    }
    
    @Override
    public void notifyPositionChange(Airplane airplane) {
        System.out.println("Tower: Tracking " + airplane.getId());
    }
}

Usage:

Tower tower = new Tower();

Airplane flight1 = new Airplane("Flight-001", tower);
Airplane flight2 = new Airplane("Flight-002", tower);

tower.registerAirplane(flight1);
tower.registerAirplane(flight2);

flight1.requestLanding();  // Cleared
flight2.requestLanding();  // Must wait

The tower (mediator) coordinates all airplane interactions. Airplanes don’t communicate directly with each other.

Mediator in Real Frameworks

Java Swing Event Handling

JButton button = new JButton("Click me");
JTextField textField = new JTextField();

// ActionListener acts as mediator
button.addActionListener(e -> {
    textField.setText("Button clicked");
});

Spring Framework

@Component
class OrderMediator {
    @Autowired
    private InventoryService inventory;
    
    @Autowired
    private PaymentService payment;
    
    @Autowired
    private ShippingService shipping;
    
    public void placeOrder(Order order) {
        // Mediator coordinates multiple services
        inventory.reserve(order);
        payment.process(order);
        shipping.schedule(order);
    }
}

When to Use Mediator

Use Mediator when:

  • Objects communicate in complex but well-defined ways
  • Reusing objects is difficult because they refer to many other objects
  • Behavior distributed among several classes should be customizable
  • You want to reduce coupling between communicating objects

Real scenarios:

  • GUI components coordination
  • Chat rooms and messaging systems
  • Air traffic control systems
  • Workflow engines
  • Event dispatching systems
  • Form validation
  • Multi-component interactions

Mediator vs Facade

Mediator: Bi-directional communication. Components know about mediator, mediator knows about components.

Facade: Uni-directional. Facade knows about subsystem, subsystem doesn’t know about facade.

Mediator: Coordinates peer-to-peer interactions.

Facade: Simplifies access to a subsystem.

Mediator vs Observer

Mediator: One-to-many relationships between specific components. Centralized control.

Observer: One-to-many dependencies. Decentralized, automatic notifications.

Mediator: “I need to coordinate these specific components.”

Observer: “Notify everyone interested when state changes.”

Can use both together: mediator uses observer pattern to notify components.

Common Pitfalls

Pitfall 1: God Object Mediator

// BAD: Mediator doing everything
class GodMediator {
    // Business logic
    // Validation logic
    // Persistence logic
    // UI logic
    // Everything!
}

Mediator should only coordinate. Don’t dump all logic into it.

Pitfall 2: Components Bypassing Mediator

// BAD: Direct communication
class Component1 {
    private Component2 component2;
    
    public void doSomething() {
        component2.doSomethingElse();  // Bypassing mediator!
    }
}

All communication should go through mediator.

Pitfall 3: Circular Dependencies

// BAD: Mediator and components tightly coupled
class Mediator {
    private Component1 c1 = new Component1(this);  // Creates component
}

class Component1 {
    public Component1(Mediator m) {
        // Also creates mediator somewhere?
    }
}

Use dependency injection to avoid circular dependencies.

Mediator and SOLID Principles

Single Responsibility Principle

Each component has its own responsibility. Mediator’s responsibility is coordination.

Open/Closed Principle

Add new components without modifying existing ones. Update mediator to coordinate new components.

Dependency Inversion Principle

Components depend on mediator interface, not concrete mediator.

The Mental Model

Think of Mediator like:

Airport control tower: Airplanes don’t talk to each other. They all talk to the tower. Tower coordinates takeoffs, landings, and airspace.

Orchestrator in music: Musicians don’t coordinate with each other directly. They all follow the conductor, who coordinates the performance.

Slack or Teams: People don’t call each other directly. They send messages through the platform, which handles routing, notifications, and coordination.

Performance Considerations

Mediator can become a bottleneck:

  • All communication goes through one point
  • Complex coordination logic in one place

Mitigations:

  • Keep mediator logic simple
  • Consider multiple mediators for different concerns
  • Use asynchronous communication if appropriate

Testing with Mediator

@Test
public void testMediatorCoordinatesComponents() {
    ChatMediator mediator = new ChatRoom();
    User alice = new User("Alice", mediator);
    User bob = new User("Bob", mediator);
    
    mediator.addUser(alice);
    mediator.addUser(bob);
    
    alice.send("Hello");
    
    // Verify bob received message (through mediator)
}

@Test
public void testFormValidation() {
    RegistrationFormMediator form = new RegistrationFormMediator();
    
    assertFalse(form.getSubmitButton().isEnabled());
    
    form.getUsername().setValue("alice");
    form.getPassword().setValue("password123");
    form.getConfirmPassword().setValue("password123");
    form.getTermsCheckbox().setValue("true");
    
    assertTrue(form.getSubmitButton().isEnabled());
}

Final Thoughts

The Mediator pattern is about reducing chaos. Instead of n objects all talking to each other (n² connections), they all talk to one mediator (n connections).

It’s not about adding complexity. It’s about:

  • Reducing coupling between components
  • Centralizing coordination logic
  • Making systems easier to maintain
  • Enabling component reuse

The key insight: objects shouldn’t know about each other’s existence. A mediator coordinates their interactions.

Remember:

  • Mediator knows about all components
  • Components only know about mediator
  • Coordination logic lives in mediator
  • Don’t make mediator a god object

Next time you have objects all coupled to each other, stop. Think Mediator. Create a coordinator that handles their interactions.

Mediate wisely.