🧩

Observer Pattern

Design Patterns Intermediate 1 min read 100 words

Define a one-to-many dependency so that when one object changes state, all dependents are notified

Observer Pattern

Intent

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

Problem

You need multiple objects to react when another object changes, without tight coupling between them.

Classic Implementation

// Observer interface
public interface IObserver<T>
{
    void Update(T data);
}

// Subject interface
public interface ISubject<T>
{
    void Attach(IObserver<T> observer);
    void Detach(IObserver<T> observer);
    void Notify(T data);
}

// Concrete subject
public class StockTicker : ISubject<StockPrice>
{
    private readonly List<IObserver<StockPrice>> _observers = new();
    private readonly Dictionary<string, decimal> _prices = new();

    public void Attach(IObserver<StockPrice> observer) => _observers.Add(observer);
    public void Detach(IObserver<StockPrice> observer) => _observers.Remove(observer);

    public void Notify(StockPrice data)
    {
        foreach (var observer in _observers)
        {
            observer.Update(data);
        }
    }

    public void UpdatePrice(string symbol, decimal price)
    {
        _prices[symbol] = price;
        Notify(new StockPrice { Symbol = symbol, Price = price, Timestamp = DateTime.UtcNow });
    }
}

// Concrete observers
public class PriceDisplay : IObserver<StockPrice>
{
    public void Update(StockPrice data)
    {
        Console.WriteLine($"[Display] {data.Symbol}: ${data.Price}");
    }
}

public class PriceLogger : IObserver<StockPrice>
{
    public void Update(StockPrice data)
    {
        Console.WriteLine($"[Log] {data.Timestamp:HH:mm:ss} - {data.Symbol}: ${data.Price}");
    }
}

public class PriceAlert : IObserver<StockPrice>
{
    private readonly Dictionary<string, decimal> _thresholds = new()
    {
        ["AAPL"] = 150m,
        ["GOOGL"] = 2500m
    };

    public void Update(StockPrice data)
    {
        if (_thresholds.TryGetValue(data.Symbol, out var threshold) && data.Price > threshold)
        {
            Console.WriteLine($"[ALERT] {data.Symbol} exceeded ${threshold}! Current: ${data.Price}");
        }
    }
}

// Usage
var ticker = new StockTicker();
ticker.Attach(new PriceDisplay());
ticker.Attach(new PriceLogger());
ticker.Attach(new PriceAlert());

ticker.UpdatePrice("AAPL", 145m);
ticker.UpdatePrice("AAPL", 155m); // Triggers alert
ticker.UpdatePrice("GOOGL", 2600m); // Triggers alert

C# Events (Preferred Approach)

C# has built-in support for the Observer pattern through events:

// Event arguments
public class OrderEventArgs : EventArgs
{
    public Order Order { get; init; }
    public string Action { get; init; }
}

// Subject with events
public class OrderService
{
    // Event declarations
    public event EventHandler<OrderEventArgs> OrderCreated;
    public event EventHandler<OrderEventArgs> OrderShipped;
    public event EventHandler<OrderEventArgs> OrderDelivered;
    public event EventHandler<OrderEventArgs> OrderCancelled;

    public void CreateOrder(Order order)
    {
        // Business logic
        order.Status = OrderStatus.Created;
        order.CreatedAt = DateTime.UtcNow;

        // Raise event - notify all subscribers
        OrderCreated?.Invoke(this, new OrderEventArgs
        {
            Order = order,
            Action = "Created"
        });
    }

    public void ShipOrder(Order order)
    {
        order.Status = OrderStatus.Shipped;
        order.ShippedAt = DateTime.UtcNow;

        OrderShipped?.Invoke(this, new OrderEventArgs
        {
            Order = order,
            Action = "Shipped"
        });
    }

    public void DeliverOrder(Order order)
    {
        order.Status = OrderStatus.Delivered;
        order.DeliveredAt = DateTime.UtcNow;

        OrderDelivered?.Invoke(this, new OrderEventArgs
        {
            Order = order,
            Action = "Delivered"
        });
    }
}

// Observer classes subscribe to events
public class EmailNotifier
{
    private readonly IEmailService _emailService;

    public EmailNotifier(IEmailService emailService, OrderService orderService)
    {
        _emailService = emailService;

        // Subscribe to events
        orderService.OrderCreated += OnOrderCreated;
        orderService.OrderShipped += OnOrderShipped;
        orderService.OrderDelivered += OnOrderDelivered;
    }

    private void OnOrderCreated(object sender, OrderEventArgs e)
    {
        _emailService.SendAsync(e.Order.CustomerEmail,
            "Order Confirmation",
            $"Your order #{e.Order.Id} has been received.");
    }

    private void OnOrderShipped(object sender, OrderEventArgs e)
    {
        _emailService.SendAsync(e.Order.CustomerEmail,
            "Order Shipped",
            $"Your order #{e.Order.Id} is on its way!");
    }

    private void OnOrderDelivered(object sender, OrderEventArgs e)
    {
        _emailService.SendAsync(e.Order.CustomerEmail,
            "Order Delivered",
            $"Your order #{e.Order.Id} has been delivered.");
    }
}

public class InventoryUpdater
{
    private readonly IInventoryService _inventory;

    public InventoryUpdater(IInventoryService inventory, OrderService orderService)
    {
        _inventory = inventory;
        orderService.OrderCreated += OnOrderCreated;
    }

    private void OnOrderCreated(object sender, OrderEventArgs e)
    {
        foreach (var item in e.Order.Items)
        {
            _inventory.ReserveStock(item.ProductId, item.Quantity);
        }
    }
}

public class AnalyticsTracker
{
    private readonly IAnalyticsService _analytics;

    public AnalyticsTracker(IAnalyticsService analytics, OrderService orderService)
    {
        _analytics = analytics;
        orderService.OrderCreated += (s, e) => _analytics.Track("OrderCreated", e.Order.Id);
        orderService.OrderShipped += (s, e) => _analytics.Track("OrderShipped", e.Order.Id);
        orderService.OrderDelivered += (s, e) => _analytics.Track("OrderDelivered", e.Order.Id);
    }
}

IObservable (Reactive Extensions)

.NET provides IObservable<T> and IObserver<T> for reactive programming:

public class TemperatureSensor : IObservable<TemperatureReading>
{
    private readonly List<IObserver<TemperatureReading>> _observers = new();

    public IDisposable Subscribe(IObserver<TemperatureReading> observer)
    {
        if (!_observers.Contains(observer))
            _observers.Add(observer);

        return new Unsubscriber(_observers, observer);
    }

    public void ReportTemperature(double celsius)
    {
        var reading = new TemperatureReading
        {
            Temperature = celsius,
            Timestamp = DateTime.UtcNow
        };

        foreach (var observer in _observers)
        {
            if (celsius > 40)
                observer.OnError(new OverheatException(celsius));
            else
                observer.OnNext(reading);
        }
    }

    public void EndTransmission()
    {
        foreach (var observer in _observers)
            observer.OnCompleted();

        _observers.Clear();
    }

    private class Unsubscriber : IDisposable
    {
        private readonly List<IObserver<TemperatureReading>> _observers;
        private readonly IObserver<TemperatureReading> _observer;

        public Unsubscriber(
            List<IObserver<TemperatureReading>> observers,
            IObserver<TemperatureReading> observer)
        {
            _observers = observers;
            _observer = observer;
        }

        public void Dispose()
        {
            if (_observer != null)
                _observers.Remove(_observer);
        }
    }
}

public class TemperatureMonitor : IObserver<TemperatureReading>
{
    private IDisposable _unsubscriber;

    public void Subscribe(IObservable<TemperatureReading> provider)
    {
        _unsubscriber = provider.Subscribe(this);
    }

    public void OnNext(TemperatureReading value)
    {
        Console.WriteLine($"Temperature: {value.Temperature}°C at {value.Timestamp:HH:mm:ss}");
    }

    public void OnError(Exception error)
    {
        Console.WriteLine($"Error: {error.Message}");
    }

    public void OnCompleted()
    {
        Console.WriteLine("Transmission complete");
        _unsubscriber?.Dispose();
    }
}

// Usage
var sensor = new TemperatureSensor();
var monitor = new TemperatureMonitor();
monitor.Subscribe(sensor);

sensor.ReportTemperature(25.5);
sensor.ReportTemperature(30.0);
sensor.ReportTemperature(45.0); // Triggers OnError
sensor.EndTransmission();

Real-World Example: Domain Events

// Domain event interface
public interface IDomainEvent
{
    DateTime OccurredAt { get; }
}

// Specific events
public record UserRegisteredEvent(int UserId, string Email, DateTime OccurredAt) : IDomainEvent;
public record UserLoggedInEvent(int UserId, DateTime OccurredAt) : IDomainEvent;
public record PasswordChangedEvent(int UserId, DateTime OccurredAt) : IDomainEvent;

// Event handler interface
public interface IEventHandler<T> where T : IDomainEvent
{
    Task HandleAsync(T @event);
}

// Event handlers
public class SendWelcomeEmailHandler : IEventHandler<UserRegisteredEvent>
{
    private readonly IEmailService _email;

    public SendWelcomeEmailHandler(IEmailService email) => _email = email;

    public async Task HandleAsync(UserRegisteredEvent @event)
    {
        await _email.SendWelcomeAsync(@event.Email);
    }
}

public class CreateUserProfileHandler : IEventHandler<UserRegisteredEvent>
{
    private readonly IProfileService _profiles;

    public CreateUserProfileHandler(IProfileService profiles) => _profiles = profiles;

    public async Task HandleAsync(UserRegisteredEvent @event)
    {
        await _profiles.CreateDefaultProfileAsync(@event.UserId);
    }
}

public class LogLoginHandler : IEventHandler<UserLoggedInEvent>
{
    private readonly ILogger _logger;

    public LogLoginHandler(ILogger logger) => _logger = logger;

    public Task HandleAsync(UserLoggedInEvent @event)
    {
        _logger.LogInformation("User {UserId} logged in at {Time}",
            @event.UserId, @event.OccurredAt);
        return Task.CompletedTask;
    }
}

// Event dispatcher
public class EventDispatcher
{
    private readonly IServiceProvider _services;

    public EventDispatcher(IServiceProvider services) => _services = services;

    public async Task DispatchAsync<T>(T @event) where T : IDomainEvent
    {
        var handlers = _services.GetServices<IEventHandler<T>>();

        foreach (var handler in handlers)
        {
            await handler.HandleAsync(@event);
        }
    }
}

// Registration
services.AddTransient<IEventHandler<UserRegisteredEvent>, SendWelcomeEmailHandler>();
services.AddTransient<IEventHandler<UserRegisteredEvent>, CreateUserProfileHandler>();
services.AddTransient<IEventHandler<UserLoggedInEvent>, LogLoginHandler>();
services.AddSingleton<EventDispatcher>();

// Usage
public class UserService
{
    private readonly EventDispatcher _dispatcher;

    public async Task RegisterAsync(string email, string password)
    {
        // Create user...
        var userId = 1;

        // Dispatch event - all handlers will be notified
        await _dispatcher.DispatchAsync(
            new UserRegisteredEvent(userId, email, DateTime.UtcNow));
    }
}

Interview Tips

Common Questions:

  • “Explain Observer pattern with an example”
  • “How does Observer relate to C# events?”
  • “What’s the difference between Observer and Pub/Sub?”

Key Points:

  1. Observer is for one-to-many notifications
  2. C# events are built-in Observer implementation
  3. IObservable<T> provides reactive programming support
  4. Decouples publisher from subscribers
  5. Used extensively in UI and event-driven systems

Observer vs Pub/Sub:

  • Observer: Direct reference between subject and observers
  • Pub/Sub: Message broker mediates, looser coupling

.NET Examples:

  • C# events and delegates
  • INotifyPropertyChanged (WPF/MAUI binding)
  • IObservable<T> (Reactive Extensions)
  • SignalR (real-time notifications)

📚 Related Articles