🤖▶️ Check out the Design Patterns Overview course by Steve @ardalis Smith!Check it out »Hide

DevIQ

Mediator Pattern

Mediator Pattern

The Mediator pattern is a design pattern that facilitates communication between different components in a system without them needing to know about each other directly. Instead of components interacting with each other directly, they communicate through a central mediator, which manages the interactions and orchestration. This helps reduce dependencies, promote loose coupling, and make the system more modular and flexible. Commonly used in complex applications where many objects interact with one another—such as in user interfaces, messaging systems, or eCommerce applications—the Mediator pattern centralizes communication logic, making components easier to manage, extend, and test. This pattern is especially useful in large-scale systems, where reducing direct dependencies is essential for maintainability and scalability.

An eCommerce Example of the Mediator

Consider this eCommerce situation:

  • PlaceOrderCommand triggers the PlaceOrderHandler to place the order.
  • PlaceOrderHandler notifies the Mediator to check the inventory.
  • CheckInventoryHandler checks inventory and then requests the Mediator to send a notification.
  • SendNotificationHandler completes the process by sending a confirmation notification.

In this example:

  • The Mediator coordinates between modules.
  • When an order is placed, the Order Service notifies the Mediator.
  • The Mediator then informs the Inventory Service to check stock.
  • Upon stock verification, the Mediator requests the Notification Service to send a confirmation.

Notification
Inventory
Mediator
Order
notify
check stock
stock confirmed
send confirmation
Notification Service: Send Confirmation
Inventory Service: Verify Stock
Mediator
Order Service: Place Order

The Order Service doesn't need to know about the Inventory Service or the Notification Service. Its sole focus is on order-related things. Once the Order Service is done placing the order, it can notify others of "Hey! I'm done with this work!" and then whoever needs to step in can step in. But the Order Service doesn't call to the Inventory Service or the Notification Service. All of that is handled by the conductor in the middle - the Mediator.

Scenarios Best Suited for the Mediator Pattern

The Mediator pattern is beneficial when:

  • There are multiple components that need to communicate in complex ways, especially when adding more components is expected.
  • Components need to be decoupled to allow easier changes in individual modules without affecting others.
  • You want to encapsulate workflows or orchestrations that would otherwise be spread across multiple classes.

Benefits of the Mediator Pattern

There are many benefits that come from using the Mediator pattern.

Loose Coupling

Components don't need to know about each other; they only interact through the Mediator. This promotes modularity and makes the system easier to manage, especially when adding or modifying components.

Improved Readability and Maintenance

The Mediator centralizes communication logic, simplifying the understanding of interactions. With complex flows, having one place where all interactions are managed can make the system more maintainable.

Easier Debugging and Testing

Since the Mediator encapsulates the communication between objects, testing interactions becomes simpler. You can mock or replace the Mediator in tests, focusing only on specific interactions without needing to initialize dependencies for each component.

Encourages Single Responsibility Principle

By removing communication responsibilities from individual components, each class focuses solely on its main function, adhering to SRP. In an eCommerce example, the OrderService is focused only on order-related logic, not on handling notifications or inventory checks.

Scalability

As the number of interacting components increases, the Mediator pattern prevents exponential growth in dependencies by centralizing connections. This is especially helpful in complex systems like ERP or eCommerce platforms where many modules need to interact.

Drawbacks of the Mediator Pattern

While there are many benefits to using the Mediator pattern, there are drawbacks to be aware of as well.

Mediator Complexity

If the Mediator becomes responsible for too many interactions, it can become overly complex and monolithic, leading to a "god object" where the Mediator itself becomes hard to manage. This can offset some of the maintainability and readability benefits.

Single Point of Failure

Since all interactions go through the Mediator, it becomes a critical component. If the Mediator fails, the entire interaction flow breaks. It also adds another layer of potential performance bottlenecks, as every request passes through it.

Harder to Trace Distributed Systems

In large systems or microservices, tracing or debugging requests across modules can become challenging, as each service is abstracted from others. This could make it harder to diagnose issues if Mediator interactions aren’t properly logged or monitored.

Limited Flexibility for Direct Interactions

The Mediator pattern may introduce unnecessary overhead if some components naturally need to interact directly. For simpler interactions, having every communication mediated can be inefficient.

Potential Performance Impact

With every interaction routed through the Mediator, the pattern can add a performance cost, particularly in latency-sensitive applications. The pattern might be overkill if the interactions are simple or infrequent.

These are some other patterns you may see with the Mediator pattern.

Command Pattern

The Command pattern is often paired with Mediator in systems that execute discrete actions or commands, such as CQRS. Commands encapsulate requests as objects, which the Mediator can route to appropriate handlers. This is especially useful when using libraries like MediatR, where commands and handlers are the foundation.

In CQRS, the Mediator pattern is useful for routing commands (write operations) to specific handlers and queries (read operations) to others. This allows for independent scaling of read and write models, making it a common combination in distributed or microservices architectures.

Observer Pattern

The Observer pattern complements Mediator by allowing components to subscribe to events or updates from the Mediator. When an event occurs, the Mediator can notify subscribers without direct dependency chains, further enhancing decoupling.

Chain of Responsibility

This pattern is helpful when the Mediator needs to delegate requests across a series of handlers, each capable of handling part of the workflow or passing the request along. The Chain of Responsibility keeps individual handler responsibilities clear while allowing the Mediator to orchestrate complex processing.

Additional Resources

Edit this page on GitHub