The observer pattern (also called publish-subscribe at the object level) is a behavioral design pattern that defines a one-to-many dependency between objects. When a subject changes state, every registered observer is notified automatically. The subject does not need to know what each observer does with the update, only that they share a common notification interface.
The problem it solves
A class that owns important state often needs to trigger side effects when that state changes: send email when an order ships, update a dashboard when inventory drops, log analytics when a user signs in. Wiring each reaction directly into the subject bloats the class, couples it to details that change independently, and makes it hard to add or remove reactions without editing core logic.
The observer pattern inverts that dependency. The subject exposes subscribe and unsubscribe operations and broadcasts changes to whoever registered. New reactions arrive as new observer classes; the subject stays focused on its own state.
Structure
- Subject, maintains state and a list of observers; notifies them when state changes.
- Observer, the interface observers implement to receive updates.
- Concrete subject, the class whose state others care about.
- Concrete observers, each reacts to notifications in its own way (email, logging, cache invalidation).
flowchart LR Subject["ConcreteSubject (Order)"] -->|"notify on change"| ObserverIface["Observer interface"] Email["EmailNotifier"] -.->|"implements"| ObserverIface Analytics["AnalyticsTracker"] -.->|"implements"| ObserverIface Inventory["InventoryUpdater"] -.->|"implements"| ObserverIface Subject -->|"holds list of"| Email Subject -->|"holds list of"| Analytics Subject -->|"holds list of"| Inventory
In code
An Order tracks status. When status changes, every subscribed observer is notified. The order class does not call email or analytics APIs directly.
// Observer: common notification interface
class EmailNotifier {
onOrderStatusChanged(orderId, status) {
console.log(`[email] order ${orderId} is now ${status}`);
}
}
class AnalyticsTracker {
onOrderStatusChanged(orderId, status) {
console.log(`[analytics] track status=${status} for ${orderId}`);
}
}
// Subject: owns state and notifies observers
class Order {
constructor(id) {
this.id = id;
this.status = "pending";
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter((o) => o !== observer);
}
setStatus(status) {
if (this.status === status) return;
this.status = status;
for (const observer of this.observers) {
observer.onOrderStatusChanged(this.id, status);
}
}
}
const order = new Order("ord-42");
order.subscribe(new EmailNotifier());
order.subscribe(new AnalyticsTracker());
order.setStatus("shipped");// Observer: common notification interface
interface OrderObserver {
onOrderStatusChanged(orderId: string, status: string): void;
}
class EmailNotifier implements OrderObserver {
onOrderStatusChanged(orderId: string, status: string): void {
console.log(`[email] order ${orderId} is now ${status}`);
}
}
class AnalyticsTracker implements OrderObserver {
onOrderStatusChanged(orderId: string, status: string): void {
console.log(`[analytics] track status=${status} for ${orderId}`);
}
}
// Subject: owns state and notifies observers
class Order {
private observers: OrderObserver[] = [];
constructor(
private readonly id: string,
private status = "pending"
) {}
subscribe(observer: OrderObserver): void {
this.observers.push(observer);
}
unsubscribe(observer: OrderObserver): void {
this.observers = this.observers.filter((o) => o !== observer);
}
setStatus(status: string): void {
if (this.status === status) return;
this.status = status;
for (const observer of this.observers) {
observer.onOrderStatusChanged(this.id, status);
}
}
}
const order = new Order("ord-42");
order.subscribe(new EmailNotifier());
order.subscribe(new AnalyticsTracker());
order.setStatus("shipped");// Observer: common notification interface
public interface IOrderObserver
{
void OnOrderStatusChanged(string orderId, string status);
}
public class EmailNotifier : IOrderObserver
{
public void OnOrderStatusChanged(string orderId, string status) =>
Console.WriteLine($"[email] order {orderId} is now {status}");
}
public class AnalyticsTracker : IOrderObserver
{
public void OnOrderStatusChanged(string orderId, string status) =>
Console.WriteLine($"[analytics] track status={status} for {orderId}");
}
// Subject: owns state and notifies observers
public class Order
{
private readonly List<IOrderObserver> _observers = new();
private string _status;
public Order(string id, string status = "pending")
{
Id = id;
_status = status;
}
public string Id { get; }
public void Subscribe(IOrderObserver observer) => _observers.Add(observer);
public void Unsubscribe(IOrderObserver observer) => _observers.Remove(observer);
public void SetStatus(string status)
{
if (_status == status) return;
_status = status;
foreach (var observer in _observers)
observer.OnOrderStatusChanged(Id, status);
}
}
var order = new Order("ord-42");
order.Subscribe(new EmailNotifier());
order.Subscribe(new AnalyticsTracker());
order.SetStatus("shipped");from typing import Protocol
# Observer: common notification interface
class OrderObserver(Protocol):
def on_order_status_changed(self, order_id: str, status: str) -> None: ...
class EmailNotifier:
def on_order_status_changed(self, order_id: str, status: str) -> None:
print(f"[email] order {order_id} is now {status}")
class AnalyticsTracker:
def on_order_status_changed(self, order_id: str, status: str) -> None:
print(f"[analytics] track status={status} for {order_id}")
# Subject: owns state and notifies observers
class Order:
def __init__(self, order_id: str, status: str = "pending") -> None:
self._id = order_id
self._status = status
self._observers: list[OrderObserver] = []
def subscribe(self, observer: OrderObserver) -> None:
self._observers.append(observer)
def unsubscribe(self, observer: OrderObserver) -> None:
self._observers = [o for o in self._observers if o is not observer]
def set_status(self, status: str) -> None:
if self._status == status:
return
self._status = status
for observer in self._observers:
observer.on_order_status_changed(self._id, status)
order = Order("ord-42")
order.subscribe(EmailNotifier())
order.subscribe(AnalyticsTracker())
order.set_status("shipped")Push vs pull
Observers can receive updates in two styles:
- Push, the subject passes the new state (or a rich event object) in the notification call. Simple for observers, but the subject must guess what data every observer needs.
- Pull, the notification is a lightweight signal; observers query the subject for the details they care about. Keeps the subject ignorant of observer needs, but observers must know the subject’s API.
Most in-process implementations push enough context to avoid extra round trips. Pull fits when observers need different slices of a large state object.
Relationship to SOLID
Observer applies the SOLID principles. It supports the open/closed principle because new reactions are new observer classes, not edits to the subject. It follows single responsibility by keeping notification plumbing in the subject and reaction logic in each observer. It also aligns with dependency inversion when both sides depend on the observer abstraction rather than concrete notifier types.
Observer vs alternatives
- Event-driven architecture, scales the same producer/consumer idea across services with a message broker, durability, and replay. Observer is the in-process, single-address-space version; EDA is the distributed one.
- Strategy pattern, one context delegates to one interchangeable algorithm. Observer fans out one state change to many listeners with different reactions.
- State pattern, an object delegates behavior to a state object that may change over time. Observer notifies external objects about changes; state changes internal behavior.
- Mediator, a mediator centralizes how objects communicate so they do not reference each other directly. Observer lets the subject know its subscribers; a mediator routes messages between peers without a single owning subject.
- Reactive streams / Rx, libraries such as RxJS or ReactiveX generalize push-based notification with operators, scheduling, and backpressure. Use them when notification graphs grow beyond a simple list of callbacks.
Trade-offs
- Notification order is usually undefined unless the subject documents it. Observers should not depend on each other running first.
- A slow or failing observer can block or break others if notification is synchronous. Consider isolating heavy work (queue, async dispatch) or catching errors per observer.
- Memory leaks are common when observers forget to unsubscribe, especially in UI frameworks where widgets outlive the subjects they watch. Weak references or explicit lifecycle hooks help.
- Large fan-out lists make debugging harder; tracing which observer reacted to a change requires logging or tooling.
- Best when one object’s state change should trigger several independent reactions, when those reactions should be added or removed without editing the subject, or when you want a stepping stone toward event-driven architecture inside a single process.
