Observer vs Mediator Pattern in C#: Key Differences Explained
Both the observer and mediator patterns deal with communication between objects, and developers regularly confuse them. They're both behavioral design patterns from the Gang of Four catalog. They both reduce direct dependencies between components. So why does it matter which one you pick? The observer vs mediator pattern in C# distinction comes down to how communication is structured -- and getting it wrong leads to designs that fight against the problem you're solving.
In this article, we'll break down each pattern individually, compare them side by side using the same chat system domain, and give you clear guidance on when to reach for one over the other. If you want to see how observer and mediator fit alongside the full catalog of GoF patterns, check out The Big List of Design Patterns for a comprehensive overview.
Quick Refresher: What Is the Observer Pattern?
The observer pattern establishes a one-to-many dependency between objects. One object -- the subject (or publisher) -- maintains a list of dependents called observers (or subscribers). When the subject's state changes, it broadcasts a notification to all registered observers. Each observer reacts independently to that notification.
The critical characteristic is the direction of communication: the subject broadcasts outward and doesn't know or care what observers do with the notification. Observers subscribe themselves and handle updates however they see fit. The subject's job ends once it sends the notification.
Here's the core mechanic in pseudocode form: the subject maintains a list of observers, provides Subscribe and Unsubscribe methods, and iterates over all observers when publishing a value. Each observer implements a simple OnNext method that receives the published data. The subject calls every observer's method in sequence -- it doesn't select recipients or coordinate responses.
The subject publishes. The observers react. There's no coordination between observers -- they each do their own thing in isolation. This is a broadcast model, and it's the foundation of event-driven architectures.
Quick Refresher: What Is the Mediator Pattern?
The mediator pattern introduces a central coordinator -- the mediator -- that handles communication between multiple objects called colleagues (or participants). Instead of objects communicating directly with each other, they send messages to the mediator, and the mediator decides what to do with those messages. It routes, transforms, filters, or orchestrates interactions as needed.
The critical characteristic is centralized control. The mediator knows about all participants and coordinates their interactions. Participants only know about the mediator -- not about each other. This turns a complex web of many-to-many relationships into a star topology where everything flows through the center.
Here's a minimal example showing the structure:
using System;
public interface IChatMediator
{
void SendMessage(string message, ChatUser sender);
void Register(ChatUser user);
}
public class ChatUser
{
private readonly IChatMediator _mediator;
public string Name { get; }
public ChatUser(string name, IChatMediator mediator)
{
Name = name;
_mediator = mediator;
}
public void Send(string message)
{
_mediator.SendMessage(message, this);
}
public void Receive(string message, string from)
{
Console.WriteLine(
$"{Name} received from {from}: {message}");
}
}
The mediator receives the message and decides who gets it. Participants don't communicate directly -- they go through the mediator for everything. For a deeper exploration of the mediator pattern with multiple implementation approaches, check out Exploring Examples of the Mediator Pattern in C#.
The Key Difference: Communication Direction
Here's the single most important distinction in the observer vs mediator pattern in C# comparison:
- Observer = broadcast and subscribe. The subject pushes notifications outward to all subscribers. It doesn't know who they are, what they do, or how many there are. Communication flows one way: from subject to observers.
- Mediator = centralized coordination. Participants send messages to the mediator, and the mediator routes them to the appropriate recipients. Communication flows inward to the mediator, which then directs it outward. The mediator actively orchestrates the interaction.
With the observer pattern, the subject has zero knowledge of what happens after it publishes. An observer might log the event, update a UI, trigger a side effect, or do nothing at all. The subject doesn't care. With the mediator pattern, the mediator knows about all participants and makes deliberate routing decisions. It controls who receives what and can apply filtering, transformation, or conditional logic.
Think of it this way: an observer is like a radio station broadcasting a signal -- anyone with a receiver can tune in, and the station doesn't know who's listening. A mediator is like an air traffic controller -- every pilot talks to the controller, and the controller coordinates all movements while knowing about every plane.
This difference has real consequences for how your code evolves. Observer patterns scale well when you keep adding new subscribers because the subject never changes. Mediator patterns scale well when interactions between participants are complex because the coordination logic is centralized.
Side-by-Side Code Comparison
The best way to understand the observer vs mediator pattern in C# is to implement both patterns in the same domain. We'll build a simple chat system two ways: first using the observer pattern where users subscribe to a channel and receive broadcast messages, then using the mediator pattern where a chat room coordinates messages between users.
Observer Approach: Channel Broadcasting
In this version, a ChatChannel acts as the subject. Users subscribe to the channel and receive every message that's published to it. The channel doesn't know what users do with the messages -- it just broadcasts.
using System;
using System.Collections.Generic;
public interface IChatObserver
{
void OnMessageReceived(
string message, string senderName);
}
public class ChatChannel
{
private readonly List<IChatObserver> _subscribers = new();
public string ChannelName { get; }
public ChatChannel(string channelName) =>
ChannelName = channelName;
public void Subscribe(IChatObserver observer) =>
_subscribers.Add(observer);
public void Unsubscribe(IChatObserver observer) =>
_subscribers.Remove(observer);
public void PostMessage(
string message, string senderName)
{
Console.WriteLine(
$"[{ChannelName}] {senderName}: {message}");
foreach (var subscriber in _subscribers)
subscriber.OnMessageReceived(
message, senderName);
}
}
public class ChannelUser : IChatObserver
{
public string Name { get; }
public ChannelUser(string name) => Name = name;
public void OnMessageReceived(
string message, string senderName)
{
if (senderName == Name)
return;
Console.WriteLine(
$" [{Name}] New message from " +
$"{senderName}: {message}");
}
}
When Alice posts a message, the channel broadcasts it to Bob and Charlie. When Bob replies, Alice and Charlie both receive it -- even though Bob's reply was directed at Alice. The channel can't send a message to just one user. It doesn't coordinate between users. It pushes messages outward and lets each subscriber handle them independently. That's the nature of broadcast semantics.
Mediator Approach: Coordinated Chat Room
In this version, a ChatRoom acts as the mediator. Users send messages through the chat room, and the room decides who receives them. The mediator can route messages to specific users, broadcast to all, or apply any coordination logic it needs.
using System;
using System.Collections.Generic;
public interface IChatRoomMediator
{
void Register(RoomUser user);
void SendToAll(string message, RoomUser sender);
void SendToUser(
string message, RoomUser sender,
string recipientName);
}
public class ChatRoom : IChatRoomMediator
{
private readonly Dictionary<string, RoomUser> _users = new();
public string RoomName { get; }
public ChatRoom(string roomName) =>
RoomName = roomName;
public void Register(RoomUser user)
{
_users[user.Name] = user;
Console.WriteLine(
$"[{RoomName}] {user.Name} joined");
}
public void SendToAll(string message, RoomUser sender)
{
foreach (var user in _users.Values)
{
if (user.Name != sender.Name)
user.Receive(message, sender.Name);
}
}
public void SendToUser(
string message, RoomUser sender,
string recipientName)
{
if (_users.TryGetValue(
recipientName, out var recipient))
recipient.Receive(message, sender.Name);
else
Console.WriteLine(
$"[{RoomName}] User " +
$"{recipientName} not found");
}
}
public class RoomUser
{
private readonly IChatRoomMediator _chatRoom;
public string Name { get; }
public RoomUser(
string name, IChatRoomMediator chatRoom)
{
Name = name;
_chatRoom = chatRoom;
}
public void SendBroadcast(string message) =>
_chatRoom.SendToAll(message, this);
public void SendDirectMessage(
string message, string recipientName) =>
_chatRoom.SendToUser(
message, this, recipientName);
public void Receive(string message, string from)
{
Console.WriteLine(
$" [{Name}] Message from {from}: {message}");
}
}
When Alice broadcasts a message, the chat room delivers it to Bob and Charlie. When Bob sends a direct message to Alice, only Alice receives it -- Charlie never sees it.
The difference is clear. The mediator can route Bob's direct message to Alice without Charlie seeing it. The mediator knows about all participants and makes deliberate decisions about message routing. Users don't subscribe to anything -- they register with the mediator and communicate through it. This centralized coordination is the hallmark of the observer vs mediator pattern distinction.
When to Choose Observer
The observer pattern fits naturally when your communication needs are straightforward broadcast semantics. Reach for it when these conditions apply.
One-to-many broadcasting. When one source needs to notify many consumers and each consumer acts independently, the observer pattern is a direct fit. Stock price updates going to multiple display widgets, configuration changes propagating to multiple services, or domain events broadcasting to multiple handlers -- these are all classic observer scenarios.
Loose coupling is a priority. The observer pattern achieves excellent decoupling because the subject has no knowledge of its observers beyond the interface contract. You can add or remove observer types without modifying the subject. This kind of composition-based design keeps your codebase flexible as it grows.
Subscribers don't need to interact with each other. The observer pattern works best when each subscriber handles the notification in isolation. If subscribers need to know about each other or their responses need coordination, the observer pattern starts to feel strained.
Event-driven notification systems. C# has built-in support for the observer pattern through events and delegates, making it the path of least resistance for event-driven designs. The plugin architecture pattern also leverages this event-driven approach for extensible designs.
When to Choose Mediator
The mediator pattern shines when the interactions between components are complex and need active management. Reach for it when these conditions apply.
Complex many-to-many interactions. When multiple objects need to communicate with multiple other objects and those interactions have rules, the mediator centralizes that complexity. Without a mediator, N objects talking directly creates N*(N-1)/2 potential connections. The mediator reduces this to N connections with all routing logic in one place.
Components need coordinated behavior. If Object A sending a message should trigger Object B to do something only if Object C is in a certain state, you need coordination logic. Placing it in a mediator keeps each component focused on its own responsibility. This is similar to how the facade pattern simplifies complex subsystem interactions -- but the mediator actively coordinates bidirectional communication.
Reducing direct dependencies between multiple objects. When you find a cluster of objects with circular or tangled references, introducing a mediator breaks those dependencies. Each object depends only on the mediator interface, making the system easier to test by mocking the mediator.
UI component orchestration. Dialog boxes, form wizards, and complex UI panels often have components that react to each other's state. A checkbox enables a text field, which validates input, which enables a submit button. The mediator pattern is a natural fit because these relationships are inherently bidirectional and conditional.
Can You Combine Them?
Yes, and it's more common than you might think. The observer vs mediator pattern in C# isn't an either-or choice. A mediator can use the observer pattern internally for its notification mechanism. Participants register with the mediator (mediator pattern), and the mediator broadcasts certain events to interested subscribers (observer pattern).
Here's a practical example where a chat room mediator uses an observer-style event system to notify external components about room activity:
using System;
using System.Collections.Generic;
public interface IRoomActivityObserver
{
void OnActivity(string roomName, string activity);
}
public class ObservableChatRoom : IChatRoomMediator
{
private readonly Dictionary<string, RoomUser> _users = new();
private readonly List<IRoomActivityObserver> _activityObservers = new();
public string RoomName { get; }
public ObservableChatRoom(string roomName) =>
RoomName = roomName;
public void SubscribeToActivity(
IRoomActivityObserver observer) =>
_activityObservers.Add(observer);
public void Register(RoomUser user)
{
_users[user.Name] = user;
NotifyActivity($"{user.Name} joined");
}
public void SendToAll(string message, RoomUser sender)
{
foreach (var user in _users.Values)
{
if (user.Name != sender.Name)
user.Receive(message, sender.Name);
}
NotifyActivity(
$"{sender.Name} sent: {message}");
}
public void SendToUser(
string message, RoomUser sender,
string recipientName)
{
if (_users.TryGetValue(
recipientName, out var recipient))
recipient.Receive(message, sender.Name);
}
private void NotifyActivity(string activity)
{
foreach (var observer in _activityObservers)
observer.OnActivity(RoomName, activity);
}
}
In this combined approach, the ObservableChatRoom uses the mediator pattern to coordinate messages between users and the observer pattern to broadcast activity events to external monitoring components. Any class implementing IRoomActivityObserver -- an audit logger, a metrics tracker, an analytics pipeline -- can subscribe to room activity without the mediator needing to know the details. The users communicate through the mediator. The monitoring components subscribe as observers. Each pattern handles a different aspect of the system's communication needs.
This kind of layered design appears in real-world systems regularly.
Comparison Summary Table
Here's a quick-reference table summarizing the observer vs mediator pattern in C# differences:
| Feature | Observer | Mediator |
|---|---|---|
| Communication model | One-to-many broadcast | Many-to-many through central hub |
| Direction | Subject pushes to subscribers | Participants route through mediator |
| Subject/mediator awareness | Subject doesn't know what observers do | Mediator knows about all participants |
| Participant coupling | Observers don't know about each other | Participants don't know about each other |
| Routing logic | None -- everyone gets notified | Mediator decides who gets what |
| Coordination | No coordination between observers | Active coordination between participants |
| Scalability pattern | Add observers without changing subject | Add coordination rules in one place |
| Typical use cases | Events, notifications, data binding | Chat systems, UI orchestration, workflow |
| C# native support | Events, delegates, IObservable<T> |
No built-in -- implement or use libraries |
Both patterns reduce direct dependencies between objects. The difference is always in how communication is structured and who controls the flow.
Frequently Asked Questions
Is the observer pattern just a simpler version of the mediator pattern?
Not exactly. They solve different problems. The observer pattern handles one-to-many broadcast notifications where the sender doesn't control what happens next. The mediator pattern handles complex many-to-many interactions where someone needs to coordinate behavior. You wouldn't replace a mediator with an observer in a scenario requiring routing logic, and you wouldn't introduce a mediator when a simple broadcast does the job.
Can C# events replace the mediator pattern?
C# events are an implementation of the observer pattern, not the mediator pattern. Events let a publisher notify subscribers, but they don't provide centralized routing or coordination logic. You could build a mediator that uses C# events internally, but the event mechanism alone doesn't give you the routing, filtering, or orchestration that the mediator pattern provides.
How does the mediator pattern differ from a message bus?
A message bus is a broader infrastructure concept -- it handles serialization, delivery guarantees, and routing across process boundaries. The mediator pattern is a code-level design pattern that coordinates objects within a single application. A message bus might use mediator concepts internally, but it operates at a different scale. The observer vs mediator pattern in C# comparison focuses on in-process coordination rather than distributed messaging.
Which pattern is better for unit testing?
Both patterns improve testability over direct object-to-object coupling. With the observer pattern, you test the subject by subscribing a mock observer and verifying notifications. With the mediator pattern, you test each participant by mocking the mediator interface. The mediator can be slightly easier to test when interactions are complex because the coordination logic is centralized rather than distributed across observers.
Do popular C# libraries like MediatR implement the mediator pattern?
MediatR is inspired by the mediator pattern but functions as a request/response pipeline with handler dispatch. It routes a request to a single handler (or multiple notification handlers), which differs from the classic mediator that coordinates bidirectional communication between known participants. Understanding the observer vs mediator pattern in C# at the conceptual level helps you evaluate tools like MediatR more critically.
When should I refactor from observer to mediator?
Consider refactoring when your observer-based design shows coordination problems. If observers need to know about each other's state, if notification order matters and you're fighting to control it, or if you're selectively notifying certain observers -- these signal that you've outgrown the broadcast model. Moving to a mediator centralizes coordination that was scattered across observers. The transition from strategy to state pattern in C# involves a similar shift from independent behavior to coordinated state management.
Can I use both patterns in the same class hierarchy?
Yes, and the combination section of this article demonstrates exactly that. A mediator can use observer-style subscriptions for broadcast events while maintaining direct coordination for targeted interactions. The key is clarity of purpose: use observer for events where subscribers act independently, and use mediator for interactions where participants need managed communication.
Wrapping Up Observer vs Mediator in C#
The observer vs mediator pattern in C# distinction is straightforward once you see the communication models clearly. The observer pattern broadcasts notifications from one source to many subscribers, and each subscriber acts independently. The mediator pattern centralizes communication between multiple participants, actively routing and coordinating their interactions.
When you need one-to-many broadcast semantics where subscribers don't interact -- use observer. When you need many-to-many coordination where participants need managed communication -- use mediator. And when your system needs both broadcast notifications and coordinated interactions, combine them. Each pattern handles a different communication shape, and knowing which shape your problem requires is the key to choosing the right tool.

