Mediator Pattern: Centralized Communication

Picture a chat application where every user object holds direct references to every other user. When Alice sends a message, her object must iterate through references to Bob, Carol, and Dave, calling...

Key Insights

  • The Mediator pattern eliminates direct object-to-object communication by routing all interactions through a central coordinator, transforming an unmanageable web of dependencies into a star topology
  • While Mediator reduces coupling between colleagues, it concentrates complexity in one place—vigilant refactoring prevents the mediator from becoming a god object
  • The pattern shines in UI coordination, workflow orchestration, and any scenario where multiple objects need to react to each other’s state changes without knowing about each other

The Communication Chaos Problem

Picture a chat application where every user object holds direct references to every other user. When Alice sends a message, her object must iterate through references to Bob, Carol, and Dave, calling methods on each. Add Eve to the chat, and you’re updating five classes. Remove Bob, and you’re hunting down null references across the codebase.

This is the communication chaos problem: when objects communicate directly, the number of potential connections grows quadratically. Ten objects can have 45 unique pairings. Twenty objects? 190 connections to manage.

Consider this tightly coupled implementation:

public class User {
    private String name;
    private List<User> contacts = new ArrayList<>();
    
    public User(String name) {
        this.name = name;
    }
    
    public void addContact(User user) {
        contacts.add(user);
        user.contacts.add(this); // Bidirectional coupling
    }
    
    public void sendMessage(String message) {
        for (User contact : contacts) {
            contact.receive(this.name + ": " + message);
        }
    }
    
    public void receive(String message) {
        System.out.println(name + " received: " + message);
    }
}

Every user knows about every other user. Testing requires instantiating the entire network. Adding features like message filtering or logging means modifying the User class. This design doesn’t scale.

Think of air traffic control. Planes don’t communicate directly with each other to coordinate landing sequences. Instead, the control tower acts as a mediator—receiving information from all aircraft and issuing instructions. No pilot needs to know about other planes’ internal systems. The tower handles coordination.

Mediator Pattern Fundamentals

The Mediator pattern encapsulates how a set of objects interact. It promotes loose coupling by preventing objects from referring to each other explicitly, letting you vary their interaction independently.

The pattern has four key components:

  1. Mediator Interface: Declares methods for communication between colleagues
  2. Concrete Mediator: Implements coordination logic and maintains references to colleagues
  3. Colleague Interface: Defines the interface for objects that communicate through the mediator
  4. Concrete Colleagues: Implement specific behavior and communicate only via the mediator

The relationship forms a star topology: all colleagues point to the mediator, and the mediator points to all colleagues. Colleagues never reference each other.

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

public abstract class User {
    protected ChatMediator mediator;
    protected String name;
    
    public User(ChatMediator mediator, String name) {
        this.mediator = mediator;
        this.name = name;
    }
    
    public abstract void send(String message);
    public abstract void receive(String message);
    
    public String getName() {
        return name;
    }
}

The colleague (User) knows only about the mediator interface—not concrete implementations, not other colleagues. This inversion is the pattern’s power.

Implementation: Chat Room Example

Let’s build a complete chat room using the Mediator pattern:

public class ChatRoom implements ChatMediator {
    private List<User> users = new ArrayList<>();
    
    @Override
    public void addUser(User user) {
        users.add(user);
        System.out.println(user.getName() + " joined the chat");
    }
    
    @Override
    public void sendMessage(String message, User sender) {
        for (User user : users) {
            // Don't send message back to sender
            if (user != sender) {
                user.receive(sender.getName() + ": " + message);
            }
        }
    }
}

public class ChatUser extends User {
    
    public ChatUser(ChatMediator mediator, String name) {
        super(mediator, name);
    }
    
    @Override
    public void send(String message) {
        System.out.println(name + " sends: " + message);
        mediator.sendMessage(message, this);
    }
    
    @Override
    public void receive(String message) {
        System.out.println(name + " receives: " + message);
    }
}

Usage becomes straightforward:

public class Application {
    public static void main(String[] args) {
        ChatMediator chatRoom = new ChatRoom();
        
        User alice = new ChatUser(chatRoom, "Alice");
        User bob = new ChatUser(chatRoom, "Bob");
        User carol = new ChatUser(chatRoom, "Carol");
        
        chatRoom.addUser(alice);
        chatRoom.addUser(bob);
        chatRoom.addUser(carol);
        
        alice.send("Hello everyone!");
        bob.send("Hey Alice!");
    }
}

Adding features like private messaging or message history requires changes only to the mediator:

public class EnhancedChatRoom implements ChatMediator {
    private List<User> users = new ArrayList<>();
    private List<String> messageHistory = new ArrayList<>();
    
    public void sendPrivateMessage(String message, User sender, String recipientName) {
        for (User user : users) {
            if (user.getName().equals(recipientName)) {
                user.receive("[Private] " + sender.getName() + ": " + message);
                return;
            }
        }
    }
    
    public List<String> getHistory() {
        return new ArrayList<>(messageHistory);
    }
    
    @Override
    public void sendMessage(String message, User sender) {
        String formattedMessage = sender.getName() + ": " + message;
        messageHistory.add(formattedMessage);
        
        for (User user : users) {
            if (user != sender) {
                user.receive(formattedMessage);
            }
        }
    }
    // ... rest of implementation
}

The User class remains unchanged. That’s the benefit.

Mediator vs. Observer vs. Facade

These patterns often get confused because they all involve coordination between objects. Here’s how they differ:

Aspect Mediator Observer Facade
Direction Many-to-many One-to-many One-to-many
Purpose Coordinate complex interactions Notify dependents of state changes Simplify complex subsystem interface
Knowledge Mediator knows all colleagues Subject doesn’t know observer details Facade knows subsystem components
Coupling Colleagues coupled to mediator only Observers coupled to subject interface Clients coupled to facade only

Observer is about notification—when one object changes, others need to know. The subject broadcasts; observers react independently.

Mediator is about coordination—multiple objects need to interact in specific ways, and those interactions have logic that shouldn’t live in any single object.

Facade is about simplification—you have a complex subsystem and want to provide a simpler interface. It doesn’t manage interactions between subsystem components; it just hides them.

Real-World Applications

UI Component Coordination

Form validation is a classic mediator use case. Fields depend on each other: the “confirm password” field validates against “password,” the submit button enables only when all fields are valid, and error messages appear contextually.

public class FormMediator {
    private TextField emailField;
    private TextField passwordField;
    private TextField confirmPasswordField;
    private Button submitButton;
    private Label errorLabel;
    
    public void fieldChanged(TextField source) {
        boolean allValid = true;
        StringBuilder errors = new StringBuilder();
        
        if (!isValidEmail(emailField.getText())) {
            allValid = false;
            if (source == emailField) {
                errors.append("Invalid email format\n");
            }
        }
        
        if (passwordField.getText().length() < 8) {
            allValid = false;
            if (source == passwordField) {
                errors.append("Password must be at least 8 characters\n");
            }
        }
        
        if (!passwordField.getText().equals(confirmPasswordField.getText())) {
            allValid = false;
            if (source == confirmPasswordField) {
                errors.append("Passwords don't match\n");
            }
        }
        
        submitButton.setEnabled(allValid);
        errorLabel.setText(errors.toString());
    }
}

Message Brokers and Orchestration

In microservices, message brokers like RabbitMQ or Kafka act as mediators. Services publish events without knowing subscribers. The broker routes messages based on topics or queues.

Game Development

Entity component systems often use mediators for collision detection, event propagation, and state synchronization. A GameMediator might coordinate between player, enemies, projectiles, and UI without any of them knowing about each other.

Trade-offs and Anti-Patterns

Benefits

  • Reduced coupling: Colleagues depend only on the mediator interface
  • Centralized control: Interaction logic lives in one place
  • Easier testing: Mock the mediator to test colleagues in isolation
  • Flexible interactions: Change coordination without modifying colleagues

Risks

The biggest risk is the god object anti-pattern. As you add features, the mediator accumulates responsibilities. A 2000-line mediator class defeats the purpose.

The solution: decompose into sub-mediators with clear responsibilities.

// Before: Monolithic mediator
public class ApplicationMediator {
    // Handles user management, messaging, notifications, 
    // file sharing, presence, and settings... 500+ lines
}

// After: Decomposed mediators
public class UserMediator {
    private PresenceMediator presenceMediator;
    // Handles user lifecycle only
}

public class MessagingMediator {
    private NotificationMediator notificationMediator;
    // Handles message routing only
}

public class ApplicationMediator {
    private UserMediator userMediator;
    private MessagingMediator messagingMediator;
    
    // Coordinates between sub-mediators only
    // Delegates specific concerns
}

When to Avoid

Skip the Mediator pattern when:

  • Interactions are simple and unlikely to change
  • Performance is critical (the extra indirection adds overhead)
  • You have only two or three interacting objects
  • Direct communication is clearer and more maintainable

Summary and Best Practices

The Mediator pattern transforms tangled object graphs into manageable star topologies. Use it when multiple objects interact in complex ways that change over time.

Implementation guidelines:

  1. Start with an interface: Even if you have one concrete mediator, the interface enables testing and future flexibility
  2. Keep colleagues ignorant: They should know nothing about each other—only the mediator interface
  3. Watch the mediator’s size: If it exceeds 200-300 lines, consider decomposition
  4. Combine with Command: Encapsulate requests as command objects for undo/redo and logging
  5. Combine with Observer: Use observer for simple notifications within the mediator’s coordination logic

The Mediator pattern isn’t about eliminating complexity—it’s about relocating it. Concentrated complexity in a well-designed mediator beats distributed complexity across dozens of tightly coupled classes. Just keep that concentration from becoming a black hole.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.