Module 4 · 4. Behavioral Patterns and Interview Practice · Lesson 10 of 12
Observer and Mediator
Learning outcome
You will be able to: (1) choose between Observer and Mediator when you need either decentralized notifications or centralized orchestration; (2) implement safe, testable flows for notifications and coordination in modern C#; (3) recognise misuse signals and refactor appropriately.
Intuition
Observer = "I publish events; any number of subscribers can react." Use it when you want low coupling between a source and zero-or-more reactions, and when reactions are independent and best-effort.
Mediator = "I'll centralize conversation rules between collaborators." Use it when interactions require ordering, transactional safety, or when you want to test coordination logic in one place.
Example mental models:
- Observer: a news publisher with many independent subscribers. Subscribers decide what to do and subscribe/unsubscribe at will.
- Mediator: an air-traffic controller who manages who lands when, enforces rules, and tells pilots what to do so they don't coordinate peer-to-peer.
Deep dive
When to pick Observer:
- Notifications only; no ordering or transactional semantics between handlers.
- The publisher shouldn't know who reacts.
- Handlers can be added/removed at runtime.
When to pick Mediator:
- Interaction requires a sequence (A -> B -> C) or conditional branching based on intermediate results.
- You need to encapsulate business rules about collaboration for testability.
- You want to avoid coupling each component to many others.
Trade-offs:
- Observer keeps publishers small but pushes complexity into many subscribers; invisible ordering can create subtle bugs.
- Mediator centralizes complexity, improving testability but risking a "God object" if it accrues unrelated responsibilities.
Minimal illustrative pseudocode
Practical concerns in C#:
- For Observer-style events, prefer EventHandler<T> with immutable event args and clear unsubscribe paths to avoid leaks.
- For Mediator, keep orchestration logic small and delegate real work to injected services.
- Both should be covered by unit tests: subscribe/raise scenarios for Observer; end-to-end coordination scenarios for Mediator.
Failure modes
Observer failure modes:
- Silent ordering assumptions: subscribers rely on another subscriber to have run first.
- Long-running handlers blocking the publisher thread (use async/event queueing for heavy work).
- Memory leaks from unsubscribed delegates (ensure proper unsubscription, weak events, or IObservable/IObserver if suitable).
Mediator failure modes:
- Becoming a catch-all coordinator that mixes unrelated rules — turns into a God object.
- Hiding complexity: when mediator grows, it can be harder to decompose into microservices.
- Over-centralization can become a performance bottleneck if every request must pass through it.
Interview drill
Micro-questions:
- When would you pick Observer over Mediator? (Answer: simple notifications, loose coupling, no cross-handler ordering/transactional needs.)
- How do you avoid memory leaks with C# events? (Answer: explicit unsubscription, weak event patterns, or use IObservable + subscriptions that are disposed.)
- How do you test a Mediator? (Answer: inject mocked collaborators and assert mediator calls them in expected order and with expected arguments.)
Mini whiteboard tasks:
- Convert a direct-call chain A -> B -> C into a Mediator. Show where to put business rules.
- Given an event-based logging and inventory update flow, identify ordering bugs and propose fixes (use Mediator to guarantee order/transactions).
Exercises (with hints):
1) Replace an event-driven side-effect that must be transactional (payment + inventory) with a Mediator. Hint: mediator returns a result object and coordinates rollback on failure. 2) Implement weak event subscription to avoid leaks. Hint: store WeakReference to handlers or use built-in patterns.
Solutions should be unit-tested. For the Mediator scenario, assert call order via mocked collaborators.
Revision checklist
- Do notifications have independence? Use Observer.
- Do interactions require ordering/atomicity? Use Mediator.
- Are you unsubscribing events or using weak subscriptions to avoid leaks?
- Is the mediator starting to handle unrelated domains? Consider splitting into multiple mediators or a workflow engine.
Production code
Production recommendations:
- Prefer explicit interfaces and dependency injection for components (so tests can inject fakes).
- Keep EventArgs immutable.
- For heavy or slow observers, publish to a background queue (Channel<T>, durable queue) instead of running inline.
- For Mediator, keep coordination code readable and limit single-responsibility violations: each mediator should coordinate a bounded conversation.
Example production checklist:
- Are events documented (who publishes, what the semantics are)?
- Do subscribers have bounded retry/backoff behavior? Are side-effects idempotent?
- Is the mediator covered with integration tests that exercise failure/rollback?
Code walkthrough
Below is a deterministic, runnable example that demonstrates both approaches. The example is intentionally small: one Observer flow (publisher raises events; two subscribers react) and one Mediator flow (mediator enforces ordering and handles success path). The Program.cs in the provided runnable code shows why you would prefer one over the other and warns about misuse.
Note: run the included Program.cs to see deterministic console output that reflects the difference in coordination model.
Interview-style micro-tests and expected answers are in the previous sections; the included runnable example provides concrete behavior to test locally.
Executable code examples
Observer vs Mediator demo (Console)
Program.cs
using System;
// Deterministic demo of Observer vs Mediator patterns.
// The example prints why each pattern is preferable and a misuse warning.
// --- Observer implementation (event-based notifications) ---
public sealed record Order(string Id, string Item, int Quantity, decimal Amount);
public sealed class OrderPlacedEventArgs : EventArgs
{
public Order Order { get; }
public OrderPlacedEventArgs(Order order) => Order = order;
}
public sealed class OrderPublisher
{
// Use EventHandler<T> with immutable EventArgs for safety
public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;
public void PlaceOrder(Order order)
{
Console.WriteLine("--- Observer pattern: event notifications ---");
// Deterministic console log to show event creation
Console.WriteLine($"Order placed: {order.Id}, Item: {order.Item}, Qty: {order.Quantity}");
OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(order));
}
}
public sealed class LoggerObserver
{
public void OnOrderPlaced(object? sender, OrderPlacedEventArgs e)
{
// Deterministic side-effect
Console.WriteLine($"LoggerObserver: recorded order {e.Order.Id}");
}
}
public sealed class InventoryObserver
{
public void OnOrderPlaced(object? sender, OrderPlacedEventArgs e)
{
// Deterministic side-effect
Console.WriteLine($"InventoryObserver: reserved {e.Order.Quantity} of {e.Order.Item} for order {e.Order.Id}");
}
}
// --- Mediator implementation (centralized coordination) ---
public interface IMediator
{
void PlaceOrder(Order order);
}
public interface IPaymentService { bool Charge(string orderId, decimal amount); }
public interface IInventoryService { bool Reserve(string orderId, string item, int qty); }
public interface IShippingService { void Schedule(string orderId); }
public sealed class PaymentService : IPaymentService
{
public bool Charge(string orderId, decimal amount)
{
Console.WriteLine($"PaymentService: charged ${amount:0.##} for order {orderId}");
return true; // deterministic success
}
}
public sealed class InventoryService : IInventoryService
{
public bool Reserve(string orderId, string item, int qty)
{
Console.WriteLine($"InventoryService: confirmed reservation of {qty} {item} for order {orderId}");
return true; // deterministic success
}
}
public sealed class ShippingService : IShippingService
{
public void Schedule(string orderId)
{
Console.WriteLine($"ShippingService: scheduled shipment for order {orderId}");
}
}
public sealed class OrderMediator : IMediator
{
private readonly IPaymentService _payment;
private readonly IInventoryService _inventory;
private readonly IShippingService _shipping;
public OrderMediator(IPaymentService payment, IInventoryService inventory, IShippingService shipping)
{
_payment = payment;
_inventory = inventory;
_shipping = shipping;
}
public void PlaceOrder(Order order)
{
Console.WriteLine("--- Mediator pattern: centralized coordination ---");
Console.WriteLine($"Order {order.Id}: received by mediator");
// Orchestration logic centralised here (deterministic sequence)
var paid = _payment.Charge(order.Id, order.Amount);
if (!paid)
{
Console.WriteLine($"Mediator: payment failed for {order.Id}");
return;
}
var reserved = _inventory.Reserve(order.Id, order.Item, order.Quantity);
if (!reserved)
{
Console.WriteLine($"Mediator: inventory reservation failed for {order.Id}, initiating refund");
return;
}
_shipping.Schedule(order.Id);
}
}
public static class Program
{
public static void Main()
{
// Observer demo
var publisher = new OrderPublisher();
var logger = new LoggerObserver();
var inventory = new InventoryObserver();
// Subscription order is deterministic for multicast delegates
publisher.OrderPlaced += logger.OnOrderPlaced;
publisher.OrderPlaced += inventory.OnOrderPlaced;
var order1 = new Order("1001", "Widget", 3, 15.00m);
publisher.PlaceOrder(order1);
// Mediator demo
var payment = new PaymentService();
var inv = new InventoryService();
var ship = new ShippingService();
var mediator = new OrderMediator(payment, inv, ship);
var order2 = new Order("2002", "Gadget", 2, 30.00m);
mediator.PlaceOrder(order2);
// Explanations printed by the program to satisfy the requirement
Console.WriteLine("Why use Observer? Preferable: decouples publishers and subscribers for simple notification flows.");
Console.WriteLine("Observer misuse warning: don't use when you need complex ordering or transactional coordination.");
Console.WriteLine("Why use Mediator? Preferable: centralizes complex interaction logic and ordering.");
Console.WriteLine("Mediator misuse warning: avoid if it becomes a God object that mixes unrelated logic.");
}
}