Skip to main content

Observer Pattern

Definition​

The Observer defines a one-to-many dependency between objects so that when one object (the subject) changes state, all its dependents (observers) are notified and updated automatically.

Coffee Shop Example​

When a customer places an order, multiple systems need to react: the kitchen display shows the new order, the loyalty system awards points, the inventory deducts stock, and the customer gets a notification. Instead of the order knowing about every system, it simply notifies its observers.

Structure​

Observer Interface​

public record Order(string Customer, string Coffee, CupSize Size, decimal Price);

public interface IOrderObserver
{
void OnOrderPlaced(Order order);
void OnOrderReady(Order order);
}

Subject β€” The Order System​

public class OrderSubject
{
private readonly List<IOrderObserver> _observers = new();

public void Subscribe(IOrderObserver observer)
{
if (!_observers.Contains(observer))
_observers.Add(observer);
}

public void Unsubscribe(IOrderObserver observer) =>
_observers.Remove(observer);

public void NotifyOrderPlaced(Order order)
{
Console.WriteLine($"\nπŸ“¦ New order: {order.Customer}'s {order.Size} {order.Coffee}");
foreach (var observer in _observers)
observer.OnOrderPlaced(order);
}

public void NotifyOrderReady(Order order)
{
Console.WriteLine($"\nβ˜• Order ready: {order.Customer}'s {order.Size} {order.Coffee}");
foreach (var observer in _observers)
observer.OnOrderReady(order);
}
}

Concrete Observers​

public class KitchenDisplay : IOrderObserver
{
public void OnOrderPlaced(Order order) =>
Console.WriteLine($" [Kitchen] β˜• Brewing {order.Size} {order.Coffee} for {order.Customer}");

public void OnOrderReady(Order order) =>
Console.WriteLine($" [Kitchen] βœ… {order.Coffee} for {order.Customer} is ready for pickup");
}

public class LoyaltyTracker : IOrderObserver
{
private readonly Dictionary<string, int> _points = new();

public void OnOrderPlaced(Order order)
{
var earned = (int)(order.Price * 10);
_points[order.Customer] = _points.GetValueOrDefault(order.Customer) + earned;
Console.WriteLine($" [Loyalty] {order.Customer} earned +{earned} pts (total: {_points[order.Customer]})");
}

public void OnOrderReady(Order order) { } // Not interested in ready events
}

public class InventoryTracker : IOrderObserver
{
public void OnOrderPlaced(Order order) =>
Console.WriteLine($" [Inventory] Deducted supplies for {order.Coffee}");

public void OnOrderReady(Order order) { } // Not interested in ready events
}

public class CustomerNotifier : IOrderObserver
{
public void OnOrderPlaced(Order order) =>
Console.WriteLine($" [SMS] Hi {order.Customer}, your {order.Coffee} is being prepared!");

public void OnOrderReady(Order order) =>
Console.WriteLine($" [SMS] Hi {order.Customer}, your {order.Coffee} is ready! πŸŽ‰");
}

Client β€” Wiring It Up​

var orderSystem = new OrderSubject();

// Subscribe all observers
orderSystem.Subscribe(new KitchenDisplay());
orderSystem.Subscribe(new LoyaltyTracker());
orderSystem.Subscribe(new InventoryTracker());
orderSystem.Subscribe(new CustomerNotifier());

// Place and complete orders
orderSystem.NotifyOrderPlaced(new Order("Alice", "Latte", CupSize.Large, 4.50m));
orderSystem.NotifyOrderReady(new Order("Alice", "Latte", CupSize.Large, 4.50m));

orderSystem.NotifyOrderPlaced(new Order("Bob", "Espresso", CupSize.Small, 2.50m));
orderSystem.NotifyOrderReady(new Order("Bob", "Espresso", CupSize.Small, 2.50m));

// Output:
// πŸ“¦ New order: Alice's Large Latte
// [Kitchen] β˜• Brewing Large Latte for Alice
// [Loyalty] Alice earned +45 pts (total: 45)
// [Inventory] Deducted supplies for Latte
// [SMS] Hi Alice, your Latte is being prepared!
//
// β˜• Order ready: Alice's Large Latte
// [Kitchen] βœ… Latte for Alice is ready for pickup
// [SMS] Hi Alice, your Latte is ready! πŸŽ‰
tip

The OrderSubject doesn't know what the observers do β€” only that they implement IOrderObserver. Adding a new observer (e.g., analytics logger) requires zero changes to the order system (OCP).

.NET Built-In Observer: Events​

C# has Observer built into the language via events and delegates:

// Modern C# approach using events
public class CoffeeOrderSystem
{
// Define the event
public event EventHandler<OrderEventArgs>? OrderPlaced;
public event EventHandler<OrderEventArgs>? OrderReady;
}

public record OrderEventArgs(Order Order) : EventArgs;

// Subscribing
var system = new CoffeeOrderSystem();
system.OrderPlaced += (sender, args) =>
Console.WriteLine($"[Kitchen] Brewing {args.Order.Coffee}");
system.OrderReady += (sender, args) =>
Console.WriteLine($"[SMS] {args.Order.Coffee} is ready!");

// Raising
system.OrderPlaced?.Invoke(system, new OrderEventArgs(order));

IObservable<T> β€” Reactive Extensions​

.NET has a formal IObservable<T> / IObserver<T> interface pair:

// Using System.Reactive (Rx.NET)
var orderStream = new Subject<Order>();

orderStream.Subscribe(order =>
Console.WriteLine($"[Kitchen] {order.Coffee}"));

orderStream.Subscribe(order =>
Console.WriteLine($"[Loyalty] +{(int)(order.Price * 10)} pts"));

orderStream.OnNext(new Order("Alice", "Latte", CupSize.Large, 4.50m));

Observer vs Mediator​

ObserverMediator
CommunicationSubject β†’ Observers (broadcast)Colleagues ↔ Mediator (two-way)
CouplingObservers know about the subjectColleagues only know the mediator
KnowledgeSubject doesn't know who observes itMediator knows all colleagues
ComplexitySimple one-to-manyCentralized multi-way communication

.NET Real-World Usage​

  • event / EventHandler β€” built-in Observer pattern in C#
  • INotifyPropertyChanged β€” WPF/MAUI data binding
  • IObservable<T> / IObserver<T> β€” Rx.NET reactive programming
  • SignalR β€” real-time notifications to connected clients
  • IProgress<T> β€” progress reporting without coupling

When to Use​

  • A change to one object requires changing others, and you don't know exactly how many
  • An object should notify others without being tightly coupled to them
  • You need event-driven architecture (pub/sub)

When NOT to Use​

  • Only one observer β€” a direct callback is simpler
  • The update chain becomes unpredictable β€” observers triggering observers causing infinite loops
  • Performance is critical and notification overhead matters

Key Takeaways​

  • Observer establishes a one-to-many relationship with automatic notification
  • Subject and observers are loosely coupled β€” subject doesn't know observer types
  • C# provides built-in support via events and IObservable<T>
  • Always unsubscribe to prevent memory leaks

Interview Questions​

Q: What's the difference between Observer and Pub/Sub? Observer is direct β€” the subject calls observer methods. Pub/Sub introduces a message broker between publisher and subscriber, making them completely decoupled. Observer is synchronous by default; Pub/Sub is typically asynchronous.

Q: How do you prevent memory leaks in Observer? Observers hold a reference to the subject (via the event). If the observer isn't unsubscribed, the GC can't collect it. Always call -= to unsubscribe, or use weak reference patterns (WeakEventManager in WPF).

Q: How does IObservable<T> differ from IEnumerable<T>? IEnumerable<T> is pull-based β€” you iterate to get items. IObservable<T> is push-based β€” items come to you as they're produced. They're duals of each other (duality principle in reactive programming).