Mediator vs Observer Pattern in C#: Key Differences Explained
When two or more objects need to communicate, you have choices about how to structure that communication. The mediator pattern and the observer pattern both address this problem, but they take fundamentally different approaches. Understanding the mediator vs observer pattern in C# comparison helps you avoid designs that fight against the communication shape your problem actually requires. Pick the wrong one and you'll spend time working around the pattern instead of benefiting from it.
In this article, we'll examine both patterns side by side using the same notification system domain. We'll compare how each pattern handles coupling, routing, scalability, and coordination -- and give you concrete guidance on when to choose one over the other. We'll also look at how these patterns can be combined and address common questions developers ask when evaluating the mediator vs observer pattern decision.
Understanding the Mediator Pattern
The mediator pattern introduces a central coordinator that manages communication between multiple objects called colleagues or participants. Instead of objects talking directly to each other, every interaction flows through this coordinator. It receives messages, applies routing logic, and delivers them to the appropriate recipients. Participants only know about the coordinator's interface -- they have no direct references to each other.
This centralized approach turns a tangled web of many-to-many relationships into a clean star topology. All coordination logic lives in one place. When the rules for how objects interact need to change, you modify the central hub rather than hunting through scattered dependencies across multiple classes.
This pattern is especially useful when interactions between components are conditional, bidirectional, or require sequencing. Think of a form dialog where enabling one checkbox should disable a text field, validate another input, and conditionally show a submit button. Without a central coordinator, each UI component would need direct references to the others. With one, each component communicates through a single interface and the coordination logic stays centralized.
Understanding the Observer Pattern
The observer pattern establishes a one-to-many dependency between a subject (publisher) and its subscribers. When the subject's state changes, it notifies all registered subscribers. Each one independently decides how to react to that notification. The subject doesn't know or care what the subscribers do with the information -- it broadcasts and moves on.
This broadcast model is the foundation of event-driven architecture. The subject maintains a list of subscribers, provides methods for subscribing and unsubscribing, and iterates over the list when publishing. Subscribers register themselves and implement a notification interface. Communication flows one way: from subject outward to all listeners.
C# has native support for this pattern through events, delegates, and the IObservable<T> interface. This built-in support makes it the path of least resistance when you need simple publish-subscribe semantics. It works well when each subscriber acts independently and doesn't need to know about other subscribers.
The Core Distinction: Centralized Control vs Distributed Notification
The single most important difference in the mediator vs observer pattern in C# comparison comes down to who controls the communication flow.
With the mediator pattern, the coordinator is an active participant. It receives messages from one object and makes deliberate decisions about where to route them. It can filter, transform, or conditionally deliver messages. The coordinator knows about all participants and actively orchestrates their interactions. Communication flows inward to the hub, which then directs it outward selectively.
With the observer pattern, the subject is a passive broadcaster. It pushes notifications to every registered subscriber without filtering or routing. The subject doesn't know who the subscribers are, how many exist, or what they do with the notification. Communication flows outward from the subject to all listeners uniformly.
Think of it this way: a mediator is like an air traffic controller who knows every plane, their positions, and their destinations -- routing each one deliberately. An observer is like a weather broadcast station -- it transmits data and anyone tuned in receives the same signal. This distinction determines whether your communication is managed or broadcast.
Side-by-Side Code Comparison
The best way to see the mediator vs observer pattern in C# differences is to implement the same scenario both ways. We'll build a notification system where multiple services need to communicate about order events.
Observer Approach: Event Broadcasting
In this version, an OrderService acts as the subject. Other services subscribe to order events and react independently. The OrderService doesn't know what subscribers do with the notifications.
using System;
using System.Collections.Generic;
public interface IOrderObserver
{
void OnOrderPlaced(string orderId, decimal amount);
}
public class OrderService
{
private readonly List<IOrderObserver> _observers = new();
public void Subscribe(IOrderObserver observer) =>
_observers.Add(observer);
public void Unsubscribe(IOrderObserver observer) =>
_observers.Remove(observer);
public void PlaceOrder(string orderId, decimal amount)
{
Console.WriteLine(
$"Order {orderId} placed for {amount:C}");
// Broadcast to all observers
foreach (var observer in _observers)
observer.OnOrderPlaced(orderId, amount);
}
}
public class InventoryService : IOrderObserver
{
public void OnOrderPlaced(
string orderId, decimal amount)
{
Console.WriteLine(
$" [Inventory] Reserving stock for {orderId}");
}
}
public class EmailService : IOrderObserver
{
public void OnOrderPlaced(
string orderId, decimal amount)
{
Console.WriteLine(
$" [Email] Sending confirmation for {orderId}");
}
}
public class AnalyticsService : IOrderObserver
{
public void OnOrderPlaced(
string orderId, decimal amount)
{
Console.WriteLine(
$" [Analytics] Recording {amount:C} sale");
}
}
When an order is placed, the OrderService broadcasts the event to every subscriber. The inventory service, email service, and analytics service all receive the same notification and act independently. The OrderService doesn't coordinate their responses or care about execution order. It pushes the event outward and each subscriber handles it in isolation.
Mediator Approach: Coordinated Notification
In this version, an OrderMediator acts as the central coordinator. Services communicate through the mediator, which decides how to route and coordinate messages.
using System;
using System.Collections.Generic;
public interface IOrderMediator
{
void RegisterService(string name, IOrderParticipant service);
void NotifyOrderPlaced(
string orderId, decimal amount,
IOrderParticipant sender);
void NotifyInventoryConfirmed(
string orderId, IOrderParticipant sender);
}
public interface IOrderParticipant
{
void Receive(string eventType, string orderId, decimal amount);
}
public class OrderMediator : IOrderMediator
{
private readonly Dictionary<string, IOrderParticipant>
_services = new();
public void RegisterService(
string name, IOrderParticipant service)
{
_services[name] = service;
}
public void NotifyOrderPlaced(
string orderId, decimal amount,
IOrderParticipant sender)
{
Console.WriteLine(
$"[Mediator] Coordinating order {orderId}");
// Step 1: Reserve inventory first
if (_services.TryGetValue("inventory", out var inv))
inv.Receive("order_placed", orderId, amount);
// Step 2: Only send email after inventory confirms
if (_services.TryGetValue("email", out var email))
email.Receive("order_placed", orderId, amount);
// Step 3: Record analytics last
if (_services.TryGetValue("analytics", out var analytics))
analytics.Receive("order_placed", orderId, amount);
}
public void NotifyInventoryConfirmed(
string orderId, IOrderParticipant sender)
{
// Route confirmation only to email service
if (_services.TryGetValue("email", out var email))
email.Receive(
"inventory_confirmed", orderId, 0m);
}
}
public class MediatedInventoryService : IOrderParticipant
{
private readonly IOrderMediator _mediator;
public MediatedInventoryService(
IOrderMediator mediator) =>
_mediator = mediator;
public void Receive(
string eventType, string orderId, decimal amount)
{
Console.WriteLine(
$" [Inventory] Reserving stock for {orderId}");
// Notify mediator that inventory is confirmed
_mediator.NotifyInventoryConfirmed(orderId, this);
}
}
public class MediatedEmailService : IOrderParticipant
{
public void Receive(
string eventType, string orderId, decimal amount)
{
if (eventType == "inventory_confirmed")
{
Console.WriteLine(
$" [Email] Stock confirmed for {orderId}");
}
else
{
Console.WriteLine(
$" [Email] Sending confirmation for {orderId}");
}
}
}
public class MediatedAnalyticsService : IOrderParticipant
{
public void Receive(
string eventType, string orderId, decimal amount)
{
Console.WriteLine(
$" [Analytics] Recording {amount:C} sale");
}
}
Notice the difference. The coordinator controls execution order -- inventory reserves stock first, then email sends confirmation, then analytics records the sale. It can also route inventory confirmation events specifically to the email service without broadcasting to everyone. Each service communicates through the central hub rather than subscribing to a broadcast. This centralized coordination is what separates the mediator vs observer pattern approach.
Coupling Differences
Both patterns reduce coupling compared to direct object-to-object dependencies, but they achieve decoupling differently.
The observer pattern couples through an event interface. The subject depends on the IOrderObserver interface, and each subscriber implements that interface. Subscribers have no knowledge of each other and no dependency on the subject's implementation. Adding a new subscriber requires zero changes to the subject or existing ones. This makes the pattern excellent for extensibility -- similar to how dependency injection enables loose coupling through abstraction.
The mediator pattern couples through a central hub. All participants depend on the coordinator's interface, and the coordinator depends on all participant interfaces. It knows about every participant and their capabilities. Adding a new participant means updating the coordination logic. The tradeoff is that complexity moves from scattered direct dependencies into a single location, making it easier to understand and modify the overall communication flow.
The coupling tradeoff is clear: the observer approach distributes knowledge and the mediator approach centralizes it. Neither eliminates coupling entirely -- they relocate it. The question is whether your system benefits more from distributed independence or centralized control. This mirrors the broader principle of inversion of control -- both patterns invert direct dependencies, just in different directions.
When to Choose the Mediator Pattern
Reach for the mediator pattern when your communication needs involve active coordination between participants.
Complex many-to-many interactions. When multiple objects need to communicate with each other and those interactions have rules, conditions, or ordering requirements, a central coordinator handles that complexity. Without one, N objects communicating directly creates N*(N-1)/2 potential connections. The mediator reduces this to N connections through a single hub.
Coordinated workflows. When the response to one event depends on the state or output of another participant, you need coordination logic. If sending a notification to Service A should only happen after Service B confirms something, this pattern handles it naturally. The command pattern also excels at encapsulating sequential operations, and it pairs well with a central coordinator for complex workflow orchestration.
Reducing spaghetti dependencies. When you find a cluster of objects with circular references or tangled communication paths, introducing a central coordinator untangles the web. Each object depends only on the coordinator's interface, making the system easier to reason about and test.
Selective routing. When different messages should reach different recipients based on conditions, the coordinator's routing logic handles this directly. The observer pattern has no built-in mechanism for selective delivery -- it broadcasts everything to everyone.
When to Choose the Observer Pattern
Reach for the observer pattern when your communication needs are straightforward broadcast semantics without coordination.
One-to-many notifications. When one source needs to notify many consumers and each consumer acts independently, this pattern is the natural fit. Configuration change broadcasts, domain event notifications, and real-time data feeds are classic use cases.
Maximum extensibility. Adding new subscribers requires zero changes to existing code. The subject broadcasts through an interface, and any class implementing that interface can subscribe. This open-closed principle alignment makes the pattern ideal when you expect the number of subscribers to grow. This extensibility is similar to what the chain of responsibility pattern offers for processing pipelines -- both allow extension without modification.
Independent reactions. When each subscriber handles the notification in isolation without needing to coordinate with other subscribers, the broadcast model keeps things simple. Each subscriber is self-contained and testable in isolation.
Leveraging C# language features. C# events and delegates implement the observer pattern natively. If your notification needs map cleanly to event semantics, using the built-in language support avoids the overhead of a custom implementation.
Performance and Scalability Considerations
The mediator vs observer pattern in C# comparison also has practical implications for performance and scalability.
The observer pattern scales well when adding subscribers. Broadcasting to N subscribers is O(N) and adding a new subscriber doesn't affect the subject or existing ones at all. The downside surfaces when you need selective notification -- broadcasting to 100 subscribers when only 3 care about a specific event wastes cycles. You can mitigate this with filtered or typed subscriptions, but that adds complexity.
The mediator pattern scales well when adding interaction rules. New coordination logic goes into one place without modifying participants. The downside is that the coordinator can become a "god object" if coordination logic grows without discipline. A coordinator handling dozens of message types and routing rules becomes difficult to maintain. The facade pattern can suffer from a similar issue when it absorbs too much subsystem knowledge.
For high-throughput scenarios, the broadcast model can be more efficient because there's no central routing overhead. For complex interaction scenarios, the coordinator's ability to short-circuit or conditionally route messages can reduce unnecessary processing. The right choice depends on whether your bottleneck is notification volume or interaction complexity.
Combining Mediator and Observer Patterns
The mediator vs observer pattern in C# is not an either-or decision. These patterns complement each other and are frequently combined in real-world systems. A coordinator can use event-style subscriptions internally for broadcast needs while maintaining direct coordination for targeted interactions.
Here's an example where an order processing mediator uses observer events to notify external monitoring systems:
using System;
using System.Collections.Generic;
public interface IProcessingObserver
{
void OnProcessingEvent(
string eventType, string details);
}
public class ObservableOrderMediator : IOrderMediator
{
private readonly Dictionary<string, IOrderParticipant>
_services = new();
private readonly List<IProcessingObserver>
_monitors = new();
public void AddMonitor(IProcessingObserver monitor) =>
_monitors.Add(monitor);
public void RegisterService(
string name, IOrderParticipant service)
{
_services[name] = service;
BroadcastEvent("registration",
$"{name} registered");
}
public void NotifyOrderPlaced(
string orderId, decimal amount,
IOrderParticipant sender)
{
// Mediator coordination
if (_services.TryGetValue("inventory", out var inv))
inv.Receive("order_placed", orderId, amount);
if (_services.TryGetValue("email", out var email))
email.Receive("order_placed", orderId, amount);
// Observer broadcast to monitors
BroadcastEvent("order_placed",
$"Order {orderId} processed for {amount:C}");
}
public void NotifyInventoryConfirmed(
string orderId, IOrderParticipant sender)
{
if (_services.TryGetValue("email", out var email))
email.Receive(
"inventory_confirmed", orderId, 0m);
BroadcastEvent("inventory_confirmed",
$"Stock confirmed for {orderId}");
}
private void BroadcastEvent(
string eventType, string details)
{
foreach (var monitor in _monitors)
monitor.OnProcessingEvent(
eventType, details);
}
}
In this combined approach, the coordinator handles order processing between services (mediator pattern) while broadcasting processing events to monitoring components (observer pattern). Audit loggers, metrics dashboards, and alerting systems can subscribe without the coordinator needing to know their details. It manages internal coordination. The subscription mechanism handles external notification. Each pattern addresses a different communication need within the same system.
Comparison Summary Table
Here's a quick-reference summary of the mediator vs observer pattern in C# differences:
| Feature | Mediator | Observer |
|---|---|---|
| Communication model | Many-to-many through central hub | One-to-many broadcast |
| Control | Mediator actively routes messages | Subject passively broadcasts |
| Participant awareness | Mediator knows all participants | Subject doesn't know observers |
| Routing | Selective, conditional delivery | Everyone gets every notification |
| Coordination | Active coordination between objects | No coordination between subscribers |
| Adding participants | May require mediator changes | Zero changes to existing code |
| Complexity location | Centralized in mediator | Distributed across observers |
| C# native support | No built-in support | Events, delegates, IObservable<T> |
| Typical use cases | Workflow orchestration, UI panels, chat | Events, data binding, notifications |
Both patterns reduce direct dependencies. The choice depends on whether your problem needs managed coordination or independent notification.
Frequently Asked Questions
What is the main difference between mediator and observer in C#?
The main difference is communication control. The mediator pattern centralizes communication through a coordinator that actively routes messages between participants. The observer pattern broadcasts notifications from a subject to all subscribers, with no routing or coordination. The coordinator decides who gets what. The subject sends everything to everyone.
Can the mediator pattern replace the observer pattern entirely?
Technically yes -- a coordinator can broadcast messages to all participants, mimicking observer behavior. But this adds unnecessary complexity when simple broadcast semantics are all you need. Using a coordinator for pure one-to-many notification introduces centralized control where none is needed. The observer pattern is simpler, has native C# support, and is the better fit when subscribers act independently.
How does coupling differ between mediator and observer?
The observer pattern couples the subject to a subscriber interface and gives subscribers zero knowledge of each other. The mediator pattern couples all participants to a coordinator interface and gives the coordinator knowledge of all participants. Observer coupling is minimal and distributed. Mediator coupling is centralized but broader. Both are improvements over direct object-to-object dependencies.
Is MediatR an implementation of the mediator pattern?
MediatR is inspired by the mediator concept but functions more as a request/response dispatch pipeline. It routes requests to handlers rather than coordinating bidirectional communication between known participants. Classic implementations involve participants that know the coordinator and communicate through it. MediatR is closer to a message dispatcher with pipeline behaviors.
When should I refactor from observer to mediator?
Consider refactoring when your subscribers start needing coordination. If notification order matters and you're fighting to control it, if subscribers need to react based on other subscribers' state, or if you find yourself filtering notifications so only certain subscribers receive them -- these are signs that the broadcast model has outgrown your needs. Moving to a central coordinator consolidates the coordination logic that was becoming scattered. The iterator pattern also deals with controlled traversal, and understanding when traversal needs coordination helps build intuition for this decision.
Can mediator and observer patterns work together?
Absolutely. A coordinator can use event-style subscriptions for broadcast notifications while maintaining direct coordination for targeted interactions. The combined approach section of this article demonstrates this with a practical example. The coordinator handles internal routing between participants while the subscription mechanism handles external broadcast to monitoring or logging components.
Which pattern is easier to unit test?
Both patterns improve testability, but in different ways. With the observer pattern, you test the subject by subscribing a mock subscriber and verifying it receives the correct notifications. With the mediator pattern, you test each participant by mocking the coordinator interface, and test the coordinator by mocking participant interfaces. The centralized approach can be easier to test when interactions are complex because all coordination logic is in one class. The broadcast approach is easier to test when interactions are simple because each subscriber is fully independent and self-contained.
Wrapping Up Mediator vs Observer Pattern in C#
The mediator vs observer pattern in C# decision comes down to communication shape. The mediator pattern centralizes coordination through a hub that actively manages how participants interact. The observer pattern distributes notification through a broadcast model where subscribers act independently.
When your objects need managed, conditional, or sequenced communication -- choose the mediator. When your objects need simple, independent, one-to-many notification -- choose the observer. And when your system needs both broadcast events and coordinated interactions, combine them. Each pattern solves a different communication problem, and matching the pattern to the problem keeps your design clean and maintainable.

