Observer Pattern
Vaibhav • September 11, 2025
In the previous article, we explored predicate delegates - a powerful way to encapsulate decision-making logic and filter data using functions. Now we’re ready to look at a design pattern that builds on delegates and events to create reactive, loosely coupled systems: the Observer Pattern.
The Observer Pattern is one of the most widely used behavioral design patterns in software development. It
allows objects to subscribe to changes in another object and react when those changes occur. In C#, this pattern
is implemented naturally using event
and delegate
constructs, which you’ve already learned about in earlier articles.
In this article, we’ll walk through the structure of the Observer Pattern, explain how it works in C#, and build a complete example step-by-step. You’ll learn how to define subjects and observers, how to raise events, and how to handle notifications in a clean, scalable way.
What Is the Observer Pattern?
The Observer Pattern defines a one-to-many relationship between objects. One object (the subject) maintains a list of dependents (the observers) and notifies them automatically when its state changes.
This pattern is useful when multiple parts of your application need to respond to changes in a shared resource - like a data model, a sensor, or a user interface component. Instead of hardcoding dependencies, you allow observers to subscribe and react independently.
In C#, the subject typically exposes an event
, and
observers attach handlers using +=
. This makes the pattern easy to
implement and maintain.
Building the Observer Pattern in C#
Let’s build a simple example: a TemperatureSensor
class that notifies observers
when the temperature changes. We’ll define a custom EventArgs
class, an event
in the subject, and multiple observers that respond to the event.
Step 1: Define Custom EventArgs
First, we define a class to hold the temperature data. This will be passed to observers when the event is raised.
public class TemperatureChangedEventArgs : EventArgs
{
public double NewTemperature { get; }
public TemperatureChangedEventArgs(double newTemperature)
{
NewTemperature = newTemperature;
}
}
This class inherits from EventArgs
and includes a read-only property for the
new temperature value.
Step 2: Create the Subject
Next, we define the TemperatureSensor
class. It exposes an event called TemperatureChanged
and raises it whenever the temperature is updated.
public class TemperatureSensor
{
private double _temperature;
public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
public void SetTemperature(double newTemperature)
{
if (_temperature != newTemperature)
{
_temperature = newTemperature;
OnTemperatureChanged(newTemperature);
}
}
protected virtual void OnTemperatureChanged(double newTemp)
{
TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(newTemp));
}
}
The SetTemperature
method updates the internal temperature and raises the event
if the value has changed. The OnTemperatureChanged
method encapsulates the
event invocation logic.
Step 3: Create Observers
Now we define observer classes that subscribe to the sensor’s event and respond when the temperature changes.
public class DisplayUnit
{
public void Subscribe(TemperatureSensor sensor)
{
sensor.TemperatureChanged += OnTemperatureChanged;
}
private void OnTemperatureChanged(object sender, TemperatureChangedEventArgs e)
{
Console.WriteLine($"Display: Temperature is now {e.NewTemperature}°C");
}
}
public class AlertSystem
{
public void Subscribe(TemperatureSensor sensor)
{
sensor.TemperatureChanged += OnTemperatureChanged;
}
private void OnTemperatureChanged(object sender, TemperatureChangedEventArgs e)
{
if (e.NewTemperature > 30)
{
Console.WriteLine("Alert: Temperature is too high!");
}
}
}
Each observer defines a method that matches the event signature and subscribes to the sensor’s event. The DisplayUnit
simply prints the temperature, while the AlertSystem
checks for high temperatures and triggers an alert.
Step 4: Wire Everything Together
Finally, we create the sensor and observers, and simulate temperature changes.
var sensor = new TemperatureSensor();
var display = new DisplayUnit();
var alert = new AlertSystem();
display.Subscribe(sensor);
alert.Subscribe(sensor);
sensor.SetTemperature(25); // Output: Display: Temperature is now 25°C
sensor.SetTemperature(32); // Output: Display: Temperature is now 32°C
// Alert: Temperature is too high!
Each time the temperature changes, the sensor notifies all observers. They respond independently based on their own logic. This is the essence of the Observer Pattern - decoupled, reactive behavior.
Unsubscribing Observers
Observers can unsubscribe from events using the -=
operator. This is important
for memory management and avoiding unwanted notifications.
sensor.TemperatureChanged -= display.OnTemperatureChanged;
After unsubscribing, the DisplayUnit
will no longer receive updates. Always
unsubscribe when the observer is no longer needed.
Benefits of the Observer Pattern
The Observer Pattern offers several advantages:
It promotes loose coupling - the subject doesn’t need to know who the observers are or what they do. Observers can be added or removed dynamically. The pattern supports modular design, testability, and scalability.
It’s ideal for UI updates, real-time monitoring, event-driven systems, and any scenario where multiple components need to react to changes.
Best Practices
Use EventHandler<T>
for consistency and clarity. Always check for null
before raising events using ?.Invoke()
. Keep event handlers short and focused.
Unsubscribe when observers are no longer needed to avoid memory leaks.
Design your observers to be independent and reusable. Avoid hardcoding dependencies between subjects and observers - let events do the communication.
Summary
The Observer Pattern is a powerful way to build reactive systems in C#. It allows objects to subscribe to changes and respond dynamically - without tight coupling or complex dependencies. In this article, you learned how to implement the pattern using events and delegates, how to define subjects and observers, and how to manage subscriptions cleanly.
You now understand how to use the Observer Pattern to build scalable, maintainable applications that respond to change gracefully. In the next article, we’ll wrap up the chapter with Functional Programming Exercises - giving you hands-on practice with delegates, lambdas, closures, and functional design patterns.