Observer Design Pattern in C#: Complete Guide with Examples
When you need objects to react automatically to changes in another object's state, the observer design pattern in C# is the behavioral pattern you should reach for. It defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. This is the foundation of event-driven programming, and C# bakes support for it right into the language.
In this complete guide, we'll cover the observer pattern from every angle -- the classic GoF structure, a custom implementation you can build from scratch, how C# events and delegates serve as a built-in observer mechanism, and the IObservable<T>/IObserver<T> interfaces from .NET. By the end, you'll have practical code examples and a clear understanding of when and how to apply this pattern in your own projects.
What Is the Observer Design Pattern?
The observer design pattern is a behavioral design pattern from the Gang of Four (GoF) catalog that establishes a one-to-many relationship between objects. When the state of one object -- called the subject (or publisher) -- changes, all registered dependents -- called observers (or subscribers) -- are notified automatically.
The pattern solves a fundamental problem in software design: how do you keep multiple objects in sync with a changing data source without tightly coupling them together? Without the observer pattern, you'd need the subject to know the concrete types of every dependent object, creating rigid dependencies that are difficult to extend and test. The observer pattern eliminates this by introducing an abstraction layer between the subject and its observers.
Structurally, the pattern involves four participants. The Subject maintains a list of observers and provides methods to attach, detach, and notify them. The Observer defines an interface with an update method that gets called when the subject's state changes. Concrete Subjects store the state of interest and send notifications when that state changes. Concrete Observers implement the observer interface and react to state changes in whatever way makes sense for their responsibility.
The key insight is that the subject doesn't need to know anything about what its observers do with the notification. It only knows they implement the observer interface. This inversion of control keeps the subject decoupled from the rest of the system and makes it easy to add new observer types without modifying existing code.
How the Observer Pattern Works in C#
C# offers multiple ways to implement the observer pattern. You can build it from scratch with custom interfaces, leverage the language's built-in event and delegate system, or use the IObservable<T> and IObserver<T> interfaces provided by the .NET base class library. Each approach has trade-offs, and understanding all three gives you the flexibility to pick the right tool for the situation.
The custom approach teaches you the mechanics of the pattern and works in scenarios where you need full control over the notification pipeline. C# events and delegates are the idiomatic choice for most .NET applications -- they're concise, well-understood, and supported by the compiler. The IObservable<T>/IObserver<T> route provides a standardized contract and integrates with reactive programming libraries for more complex streaming scenarios.
We'll walk through all three approaches with complete, runnable examples.
Building a Custom Observer Pattern
Let's build a classic observer pattern implementation from scratch. We'll create a stock price monitor that notifies display panels whenever a stock price changes. This is a textbook example that maps cleanly to the GoF structure.
Defining the Interfaces
First, we define the contracts for both the subject and the observer:
using System;
using System.Collections.Generic;
namespace ObserverPattern.Custom;
public interface IStockSubject
{
void Attach(IStockObserver observer);
void Detach(IStockObserver observer);
void Notify();
}
public interface IStockObserver
{
void Update(string symbol, decimal price);
}
The IStockSubject interface declares the three operations every subject must support: attaching observers, detaching them, and notifying all registered observers. The IStockObserver interface defines a single Update method that receives the stock symbol and its new price.
Creating the Concrete Subject
The StockPriceMonitor class maintains a list of observers and the current stock price. When the price changes, it notifies every registered observer:
using System;
using System.Collections.Generic;
namespace ObserverPattern.Custom;
public sealed class StockPriceMonitor : IStockSubject
{
private readonly List<IStockObserver> _observers = new();
private readonly string _symbol;
private decimal _price;
public StockPriceMonitor(string symbol, decimal initialPrice)
{
_symbol = symbol;
_price = initialPrice;
}
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
Notify();
}
}
}
public void Attach(IStockObserver observer)
{
if (!_observers.Contains(observer))
{
_observers.Add(observer);
}
}
public void Detach(IStockObserver observer)
{
_observers.Remove(observer);
}
public void Notify()
{
foreach (var observer in _observers)
{
observer.Update(_symbol, _price);
}
}
}
Notice that the Price setter calls Notify() only when the value actually changes. This prevents unnecessary notifications and is a common optimization in observer pattern implementations.
Creating Concrete Observers
Now we create two different observers that react to price changes in different ways:
using System;
namespace ObserverPattern.Custom;
public sealed class PriceDisplayPanel : IStockObserver
{
private readonly string _panelName;
public PriceDisplayPanel(string panelName)
{
_panelName = panelName;
}
public void Update(string symbol, decimal price)
{
Console.WriteLine(
$"[{_panelName}] {symbol}: ${price:F2}");
}
}
public sealed class PriceAlertService : IStockObserver
{
private readonly decimal _threshold;
public PriceAlertService(decimal threshold)
{
_threshold = threshold;
}
public void Update(string symbol, decimal price)
{
if (price >= _threshold)
{
Console.WriteLine(
$"[ALERT] {symbol} has reached " +
$"${price:F2} (threshold: ${_threshold:F2})");
}
}
}
The PriceDisplayPanel simply displays the current price. The PriceAlertService only reacts when the price crosses a threshold. Both implement IStockObserver, but they handle the notification completely differently -- and the subject doesn't care. This is the composition-based flexibility that makes the observer pattern so powerful.
Putting It All Together
using ObserverPattern.Custom;
var monitor = new StockPriceMonitor("MSFT", 420.00m);
var mainDisplay = new PriceDisplayPanel("Main Lobby");
var tradingDisplay = new PriceDisplayPanel("Trading Floor");
var alertService = new PriceAlertService(425.00m);
monitor.Attach(mainDisplay);
monitor.Attach(tradingDisplay);
monitor.Attach(alertService);
monitor.Price = 422.50m;
monitor.Price = 425.75m;
monitor.Detach(tradingDisplay);
monitor.Price = 419.00m;
When the price changes to 422.50, both display panels update but the alert service stays silent because the threshold hasn't been reached. When the price hits 425.75, all three observers respond. After detaching the trading floor display, only the main lobby display and alert service receive the final update. This demonstrates the full lifecycle of the observer pattern -- registration, notification, and unsubscription.
Using C# Events and Delegates as the Observer Pattern
If you've used C# events before, you've already used the observer pattern. The event keyword, combined with delegates, is C#'s built-in implementation of the pattern. The publisher (subject) declares an event, and subscribers (observers) attach handler methods to it. The language handles the attach/detach/notify mechanics for you.
Here's the same stock price example rewritten using C# events:
using System;
namespace ObserverPattern.Events;
public sealed class StockPriceChangedEventArgs : EventArgs
{
public string Symbol { get; }
public decimal Price { get; }
public StockPriceChangedEventArgs(
string symbol,
decimal price)
{
Symbol = symbol;
Price = price;
}
}
public sealed class StockPriceMonitor
{
private decimal _price;
public StockPriceMonitor(string symbol, decimal initialPrice)
{
Symbol = symbol;
_price = initialPrice;
}
public string Symbol { get; }
public event EventHandler<StockPriceChangedEventArgs>?
PriceChanged;
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
OnPriceChanged();
}
}
}
private void OnPriceChanged()
{
PriceChanged?.Invoke(
this,
new StockPriceChangedEventArgs(Symbol, _price));
}
}
Subscribing to the event looks like this:
using ObserverPattern.Events;
var monitor = new StockPriceMonitor("MSFT", 420.00m);
monitor.PriceChanged += (sender, e) =>
Console.WriteLine(
$"[Display] {e.Symbol}: ${e.Price:F2}");
monitor.PriceChanged += (sender, e) =>
{
if (e.Price >= 425.00m)
{
Console.WriteLine(
$"[ALERT] {e.Symbol} crossed $425.00!");
}
};
monitor.Price = 422.50m;
monitor.Price = 425.75m;
The += operator is the attach operation, -= is detach, and raising the event with Invoke is the notify step. The compiler enforces that only the declaring class can raise the event, which prevents external code from triggering notifications inappropriately.
This event-based approach is the most common way to implement the observer pattern in C# applications. It's concise, well-supported by tooling, and familiar to every C# developer. However, it comes with trade-offs around memory management and async scenarios that we'll cover in the best practices section.
Observer Pattern with IObservable<T> and IObserver<T>
The .NET base class library provides the IObservable<T> and IObserver<T> interfaces as a standardized way to implement the observer pattern. These interfaces introduce a push-based notification model and use IDisposable for clean unsubscription.
Here's our stock price example using the .NET interfaces:
using System;
using System.Collections.Generic;
namespace ObserverPattern.Observable;
public sealed class StockPriceMonitor
: IObservable<StockPrice>
{
private readonly List<IObserver<StockPrice>> _observers
= new();
private readonly string _symbol;
public StockPriceMonitor(string symbol)
{
_symbol = symbol;
}
public IDisposable Subscribe(
IObserver<StockPrice> observer)
{
if (!_observers.Contains(observer))
{
_observers.Add(observer);
}
return new Unsubscriber(_observers, observer);
}
public void PublishPrice(decimal price)
{
var stockPrice = new StockPrice(_symbol, price);
foreach (var observer in _observers)
{
observer.OnNext(stockPrice);
}
}
public void EndTransmission()
{
foreach (var observer in _observers)
{
observer.OnCompleted();
}
_observers.Clear();
}
private sealed class Unsubscriber : IDisposable
{
private readonly List<IObserver<StockPrice>>
_observers;
private readonly IObserver<StockPrice> _observer;
public Unsubscriber(
List<IObserver<StockPrice>> observers,
IObserver<StockPrice> observer)
{
_observers = observers;
_observer = observer;
}
public void Dispose()
{
_observers.Remove(_observer);
}
}
}
public record StockPrice(string Symbol, decimal Price);
public sealed class StockPriceDisplay
: IObserver<StockPrice>
{
private readonly string _displayName;
public StockPriceDisplay(string displayName)
{
_displayName = displayName;
}
public void OnNext(StockPrice value)
{
Console.WriteLine(
$"[{_displayName}] {value.Symbol}: " +
$"${value.Price:F2}");
}
public void OnError(Exception error)
{
Console.WriteLine(
$"[{_displayName}] Error: {error.Message}");
}
public void OnCompleted()
{
Console.WriteLine(
$"[{_displayName}] Feed completed.");
}
}
The IObserver<T> interface provides three methods: OnNext for regular notifications, OnError for error propagation, and OnCompleted to signal the end of the data stream. This three-method contract makes the interface well-suited for streaming scenarios where the data source has a definite lifecycle.
The Subscribe method returns an IDisposable that the observer can use to unsubscribe. This is cleaner than a separate Detach method because it integrates with C#'s using statement and disposal patterns:
using ObserverPattern.Observable;
var monitor = new StockPriceMonitor("MSFT");
var display = new StockPriceDisplay("Main Lobby");
using (monitor.Subscribe(display))
{
monitor.PublishPrice(420.00m);
monitor.PublishPrice(422.50m);
}
// display is automatically unsubscribed here
monitor.PublishPrice(425.75m); // display won't see this
The IObservable<T>/IObserver<T> approach is especially valuable when you're working with reactive extensions (Rx.NET) or building systems that model data as streams of events over time. The standardized interface means any IObservable<T> source can work with any IObserver<T> consumer, giving you a universal contract for push-based notifications across your entire application.
Common Use Cases
The observer design pattern in C# appears across many domains and application types. Recognizing these use cases helps you identify opportunities to apply the pattern in your own work.
UI updates are the classic observer scenario. In desktop applications built with WPF or WinForms, the data model acts as the subject and UI controls act as observers. When the model changes, the UI refreshes automatically. This is the core mechanic behind data binding in MVVM architectures, where INotifyPropertyChanged is essentially a specialized observer interface built into .NET.
Event logging and auditing is another natural fit. A centralized event bus or application service can act as the subject, and logging observers can record every state change without the core service knowing or caring about the logging infrastructure. This keeps business logic clean and separates the cross-cutting concern of auditing into its own component.
Notification systems rely heavily on the observer pattern. When a user performs an action -- like placing an order or posting a comment -- multiple subsystems may need to react: sending an email confirmation, updating analytics, triggering a webhook, or refreshing a cache. Each of these reactions can be implemented as an independent observer, making the system easy to extend without modifying the order processing logic.
Real-time data feeds like stock tickers, sensor networks, and chat applications use the observer pattern to push data from producers to consumers. The IObservable<T> interface is particularly well-suited for these scenarios because it models the feed as a stream with explicit start, data, error, and completion signals.
Message queue consumers follow the observer pattern at an architectural level. When a message arrives on a queue, all registered consumers are notified and can process the message independently. This is the same one-to-many notification relationship that the observer pattern formalizes, scaled up to distributed systems.
These use cases share a common thread: one source of truth needs to notify multiple interested parties without knowing their concrete types. Whether you're building a simple UI form or a distributed microservices architecture, the observer pattern provides the structural foundation for decoupled, event-driven communication. You can explore how this pattern fits alongside other behavioral patterns like the mediator pattern and the strategy pattern to build flexible, maintainable systems.
Best Practices and Pitfalls
The observer pattern is conceptually simple, but production implementations require careful attention to several important concerns. Getting these right is the difference between a clean, reliable system and one plagued by subtle bugs.
Guard against memory leaks from event handler references. This is the single most common pitfall with the observer pattern in C#. When an observer subscribes to a subject's event, the subject holds a strong reference to the observer. If the observer goes out of scope but never unsubscribes, the garbage collector cannot reclaim it because the subject still references it. Over time, this leads to memory leaks that are difficult to diagnose. Always unsubscribe when an observer is no longer needed, and consider using weak events for long-lived subjects where observers may come and go frequently.
Practice unsubscription discipline. Every += should have a corresponding -=. Every Subscribe call should have a corresponding Dispose. This is especially important in applications with complex object lifecycles, like UI frameworks where views are created and destroyed as users navigate. The IObservable<T> approach helps here because the IDisposable return value makes the unsubscription contract explicit and integrable with using blocks.
Handle thread safety. If multiple threads can attach, detach, or notify observers concurrently, you need synchronization. A common approach is to copy the observer list before iterating during notification, which prevents InvalidOperationException if an observer modifies the list during iteration. For the events-based approach, the null-conditional invocation pattern (PriceChanged?.Invoke(...)) is thread-safe for the null check, but you may still need locking around subscriber management in high-concurrency scenarios.
Don't let observer exceptions break the notification chain. If one observer throws an exception during its update, should the remaining observers still be notified? In most cases, yes. Wrap individual observer calls in try-catch blocks within the Notify method so that a failing observer doesn't prevent others from receiving the notification. Log the exception and continue.
Keep observer update methods fast. The subject's Notify method typically calls observers synchronously, meaning a slow observer blocks the entire notification cycle. If an observer needs to perform expensive work -- like writing to a database or calling an external API -- have the update method queue the work for asynchronous processing rather than performing it inline. This keeps the notification pipeline responsive.
Avoid circular notification chains. If Observer A updates a subject that triggers Observer B, which updates another subject that triggers Observer A again, you get infinite recursion. Guard against this by tracking whether a notification is already in progress, or by designing your subject-observer relationships as a directed acyclic graph.
Consider the pipeline pattern for ordered processing. The observer pattern does not guarantee notification order, and observers are meant to be independent. If you need observers to execute in a specific sequence or pass data between them, the pipeline pattern is a better fit.
Frequently Asked Questions
What is the difference between the observer pattern and the pub/sub pattern?
The observer pattern and publish/subscribe (pub/sub) are closely related but differ in coupling. In the classic observer pattern, the subject directly references its observers and notifies them. In pub/sub, a message broker or event bus sits between publishers and subscribers, so they don't reference each other at all. Pub/sub is a looser coupling that's common in distributed systems and message queue architectures, while the observer pattern is typically used within a single application boundary.
When should I use IObservable<T> instead of C# events?
Use IObservable<T> when you're modeling a stream of data over time, when you need explicit completion or error signaling, or when you plan to use reactive extensions (Rx.NET) for composing and transforming event streams. C# events are simpler and more appropriate for straightforward notification scenarios where you just need to tell subscribers that something happened. If you find yourself filtering, buffering, or combining events in complex ways, IObservable<T> gives you a richer abstraction.
How do I prevent memory leaks with C# events?
Always unsubscribe event handlers when the subscribing object is no longer needed. Use the -= operator for standard events or call Dispose() on the IDisposable returned by IObservable<T>.Subscribe(). For scenarios where you cannot guarantee timely unsubscription, use weak event patterns that allow the garbage collector to reclaim observers even while they're still registered. WPF provides a built-in WeakEventManager for this purpose.
How does the observer pattern differ from the mediator pattern?
The observer pattern creates a direct one-to-many dependency between a subject and its observers. The mediator pattern centralizes communication through a mediator object, so components communicate indirectly. In the observer pattern, the subject knows it has observers (though not their concrete types). In the mediator pattern, neither party knows about the other -- they only know the mediator. Use the observer pattern for simple event notification and the mediator pattern when you need to coordinate complex interactions between multiple objects.
Can I use async methods as event handlers in C#?
You can attach async void methods as event handlers, but this comes with important caveats. The async void signature means exceptions won't be caught by the caller, and the event raiser has no way to await the handler's completion. For simple fire-and-forget scenarios this may be acceptable, but for cases where you need error handling or sequential processing, consider alternative approaches. See the detailed guide on async event handlers in C# for strategies to handle this safely.
Is INotifyPropertyChanged an example of the observer pattern?
Yes. The INotifyPropertyChanged interface in .NET is a specialized form of the observer pattern designed for data binding. The object implementing the interface is the subject, the PropertyChanged event is the notification mechanism, and the UI framework's binding engine acts as the observer. When a property value changes, the object raises the event, and the binding engine updates the corresponding UI element automatically. This is the mechanism that powers data binding in WPF, MAUI, and other XAML-based frameworks.
How does the observer pattern relate to dependency injection?
The observer pattern and dependency injection complement each other well. You can register your subject and observers in the DI container and let the container wire them together. For example, you might register an IObservable<T> as a singleton and resolve IObserver<T> implementations from the container to subscribe them during application startup. This keeps observer registration centralized and configurable, making it easy to add or remove observers without changing business logic.
Wrapping Up the Observer Design Pattern in C#
The observer design pattern in C# is a foundational behavioral pattern that every C# developer should understand. Whether you implement it through custom interfaces, C# events and delegates, or the IObservable<T>/IObserver<T> interfaces, the core concept remains the same: one object notifies many dependents about state changes without knowing their concrete types.
The pattern is deeply embedded in the .NET ecosystem. C# events are the observer pattern with language-level support. INotifyPropertyChanged is the observer pattern specialized for UI data binding. IObservable<T> is the observer pattern formalized for streaming data. Understanding the underlying pattern helps you use all of these features more effectively and recognize when to apply them.
Start by identifying places in your codebase where one component needs to inform others about state changes. If you're already using events, you're already using the observer pattern -- but pay attention to memory leak risks from unmanaged subscriptions and consider whether IObservable<T> would give you a cleaner lifecycle model. Keep your observers focused, handle errors gracefully within the notification chain, and always unsubscribe when an observer's work is done. You can find the observer pattern alongside other behavioral patterns in the complete list of design patterns.

