Skip to content

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects. When one object (the subject/publisher) changes state, all its dependents (observers/subscribers) are notified automatically.

  • Event systems (UI events, domain events)
  • Real-time notifications (stock prices, live scores)
  • Decoupling components that need to react to state changes
  • Implementing event-driven architectures
// Event interface
public interface IOrderEventHandler
{
Task HandleAsync(OrderPlacedEvent evt);
}
// Subject (publisher)
public class OrderService
{
private readonly List<IOrderEventHandler> _handlers = new();
public void Subscribe(IOrderEventHandler handler) => _handlers.Add(handler);
public void Unsubscribe(IOrderEventHandler handler) => _handlers.Remove(handler);
public async Task PlaceOrderAsync(CreateOrderRequest request)
{
// Create and save order...
var order = new Order(request);
await _orderRepository.SaveAsync(order);
// Notify all subscribers
var evt = new OrderPlacedEvent(order.Id, order.CustomerId, order.Total);
foreach (var handler in _handlers)
{
await handler.HandleAsync(evt);
}
}
}
// Observers
public class EmailNotificationHandler : IOrderEventHandler
{
public async Task HandleAsync(OrderPlacedEvent evt)
{
await _emailService.SendOrderConfirmationAsync(evt.CustomerId, evt.OrderId);
}
}
public class InventoryHandler : IOrderEventHandler
{
public async Task HandleAsync(OrderPlacedEvent evt)
{
await _inventoryService.ReserveItemsAsync(evt.OrderId);
}
}
// Setup
var orderService = new OrderService();
orderService.Subscribe(new EmailNotificationHandler());
orderService.Subscribe(new InventoryHandler());

C# has built-in event support using event and delegates:

public class StockPrice
{
public string Symbol { get; }
public decimal Price { get; private set; }
public event EventHandler<PriceChangedEventArgs>? PriceChanged;
public StockPrice(string symbol, decimal initialPrice)
{
Symbol = symbol;
Price = initialPrice;
}
public void UpdatePrice(decimal newPrice)
{
var old = Price;
Price = newPrice;
PriceChanged?.Invoke(this, new PriceChangedEventArgs(Symbol, old, newPrice));
}
}
// Subscribe
var aapl = new StockPrice("AAPL", 175.00m);
aapl.PriceChanged += (sender, args) =>
Console.WriteLine($"{args.Symbol}: {args.OldPrice} → {args.NewPrice}");
aapl.UpdatePrice(178.50m); // triggers notification
type EventMap = {
'user:registered': { userId: string; email: string };
'order:placed': { orderId: string; amount: number };
'payment:failed': { orderId: string; reason: string };
};
class EventBus {
private listeners = new Map<string, Function[]>();
on<K extends keyof EventMap>(event: K, listener: (payload: EventMap[K]) => void): void {
const list = this.listeners.get(event) ?? [];
list.push(listener);
this.listeners.set(event, list);
}
off<K extends keyof EventMap>(event: K, listener: Function): void {
const list = this.listeners.get(event) ?? [];
this.listeners.set(event, list.filter(l => l !== listener));
}
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
this.listeners.get(event)?.forEach(listener => listener(payload));
}
}
// Usage
const bus = new EventBus();
bus.on('user:registered', ({ userId, email }) => {
console.log(`Send welcome email to ${email}`);
});
bus.on('user:registered', ({ userId }) => {
console.log(`Create default preferences for user ${userId}`);
});
bus.emit('user:registered', { userId: '123', email: 'alice@example.com' });

React’s useEffect with state is the Observer pattern built-in:

// Component observes external data (subscribes to changes)
function StockTicker({ symbol }: { symbol: string }) {
const [price, setPrice] = useState<number | null>(null);
useEffect(() => {
const subscription = stockFeed.subscribe(symbol, setPrice);
return () => subscription.unsubscribe(); // cleanup = unsubscribe
}, [symbol]);
return <div>{symbol}: ${price?.toFixed(2) ?? 'Loading...'}</div>;
}

In Domain-Driven Design, domain events are a core use of the Observer pattern:

// Raise events in domain entities
public class Order
{
private readonly List<IDomainEvent> _events = new();
public IReadOnlyList<IDomainEvent> Events => _events;
public void Place()
{
Status = OrderStatus.Placed;
_events.Add(new OrderPlacedEvent(Id, CustomerId));
}
}
// Dispatch in the repository or unit of work
await orderRepository.SaveAsync(order);
foreach (var evt in order.Events)
await _mediator.PublishAsync(evt);