Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Guided course · Architecture and Design in .NET

    Design Patterns in C# with Practical Examples: back to the course

    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

    C#Runs
    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.");
        }
    }
    Sign in to mark lessons done and keep your place in the course.Sign in