Intent

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically — without the changed object knowing exactly who's listening.

The Problem

You've built a Cart. When items get added, several things need to happen: the totals display updates, inventory gets reserved, the recommendation engine refreshes, analytics fires an event, the autosave mechanism kicks in. The first cut is the obvious one:

PHP
public function addItem(Item $item): void
{
    $this->items[] = $item;

    $this->totals->recalculate($this);
    $this->inventory->reserve($item);
    $this->recommendations->refresh($this);
    $this->analytics->trackAddToCart($item);
    $this->autosave->save($this);
}

Now Cart knows about every downstream concern — every dependency in its constructor, every test importing all of them, every change to any of them potentially breaking Cart. Add a sixth listener and you're editing Cart again. Want to temporarily skip analytics in tests? You're rewiring the constructor. Want to fire two notifications conditionally? You're sprinkling ifs into a class that should only be about carts.

The Solution

Observer says: Cart doesn't call any of those collaborators directly. Instead, it maintains a list of observers and notifies them when something interesting happens. Each observer subscribes itself; the cart never learns who they are.

PHP
interface CartObserver
{
    public function itemAdded(Cart $cart, Item $item): void;
}

final class Cart
{
    /** @var CartObserver[] */
    private array $observers = [];

    public function subscribe(CartObserver $o): void { $this->observers[] = $o; }

    public function addItem(Item $item): void
    {
        $this->items[] = $item;
        foreach ($this->observers as $o) $o->itemAdded($this, $item);
    }
}

Now adding a sixth reaction is a new class implementing CartObserver plus one line in your bootstrap to subscribe it. Cart never opens.

Real-World Analogy

A magazine subscription. The publisher doesn't have a list of personal acquaintances — they maintain a subscriber list. When the next issue comes out, every subscriber receives a copy. Subscribers can join (subscribe) or leave (unsubscribe) at any time, and the publisher doesn't have to know any of them personally to deliver the magazine.

If a subscriber moves house, they update their address with the publisher; the publisher doesn't have to chase them. If the publisher gains a new subscriber today, they get tomorrow's issue without any coordination beyond the subscription itself.

Structure

Observer pattern: a Cart subject maintains a list of CartObserver subscribers; each concrete observer reacts to notifications.
Observer: the subject knows the list, not the listeners.

Four roles you'll see in every Observer implementation:

  • Subject — the object whose state changes. Holds the list of observers and notifies them. Here: Cart.
  • Observer — the interface every listener agrees on. Usually one or a few methods describing what kinds of events the subject can emit. Here: CartObserver with itemAdded().
  • Concrete Observer — a specific listener: TotalsRecalculator, InventoryReserver, AnalyticsTracker. Each one decides what to do when notified.
  • Concrete Subject — the specific subject when there's an interface for that too (often there isn't — Cart is just Cart).

The cardinal rule: the Subject knows the list, not the listeners. Adding a listener is registering on the list. The Subject never learns names.

Code Examples

Here's the same Cart notification flow in five languages. Watch how addItem shrinks to "do the local work, then walk the list."

interface CartObserver {
  itemAdded(cart: Cart, item: Item): void;
}

export class Cart {
  private observers: CartObserver[] = [];
  private items: Item[] = [];

  subscribe(observer: CartObserver): void {
    this.observers.push(observer);
  }

  addItem(item: Item): void {
    this.items.push(item);
    for (const o of this.observers) o.itemAdded(this, item);
  }
}

class AnalyticsTracker implements CartObserver {
  itemAdded(_cart: Cart, item: Item): void {
    analytics.track("add_to_cart", { sku: item.sku });
  }
}
from abc import ABC, abstractmethod

class CartObserver(ABC):
    @abstractmethod
    def item_added(self, cart, item): ...

class Cart:
    def __init__(self):
        self._observers = []
        self._items = []

    def subscribe(self, observer):
        self._observers.append(observer)

    def add_item(self, item):
        self._items.append(item)
        for o in self._observers:
            o.item_added(self, item)

class AnalyticsTracker(CartObserver):
    def item_added(self, cart, item):
        analytics.track("add_to_cart", sku=item.sku)
public interface CartObserver {
    void itemAdded(Cart cart, Item item);
}

public final class Cart {
    private final List<CartObserver> observers = new ArrayList<>();
    private final List<Item> items = new ArrayList<>();

    public void subscribe(CartObserver observer) {
        observers.add(observer);
    }

    public void addItem(Item item) {
        items.add(item);
        for (CartObserver o : observers) {
            o.itemAdded(this, item);
        }
    }
}

public final class AnalyticsTracker implements CartObserver {
    @Override
    public void itemAdded(Cart cart, Item item) {
        Analytics.track("add_to_cart", Map.of("sku", item.sku()));
    }
}
<?php

namespace App\Cart;

interface CartObserver
{
    public function itemAdded(Cart $cart, Item $item): void;
}

final class Cart
{
    /** @var CartObserver[] */
    private array $observers = [];
    /** @var Item[] */
    private array $items = [];

    public function subscribe(CartObserver $observer): void
    {
        $this->observers[] = $observer;
    }

    public function addItem(Item $item): void
    {
        $this->items[] = $item;
        foreach ($this->observers as $o) {
            $o->itemAdded($this, $item);
        }
    }
}

final class AnalyticsTracker implements CartObserver
{
    public function itemAdded(Cart $cart, Item $item): void
    {
        Analytics::track('add_to_cart', ['sku' => $item->sku]);
    }
}
package cart

type CartObserver interface {
    ItemAdded(cart *Cart, item Item)
}

type Cart struct {
    observers []CartObserver
    items     []Item
}

func (c *Cart) Subscribe(observer CartObserver) {
    c.observers = append(c.observers, observer)
}

func (c *Cart) AddItem(item Item) {
    c.items = append(c.items, item)
    for _, o := range c.observers {
        o.ItemAdded(c, item)
    }
}

// In Go, channels are often a more idiomatic way to express the same idea:
// the cart sends events on a channel, and goroutines listen to that channel.
type AnalyticsTracker struct{}

func (AnalyticsTracker) ItemAdded(_ *Cart, item Item) {
    Analytics.Track("add_to_cart", map[string]any{"sku": item.SKU})
}

The Cart constructors don't import AnalyticsTracker, InventoryReserver, or anyone else. Subscriptions happen at composition time — usually in a bootstrap or a service provider. Adding a sixth observer is one new class plus one line of wiring.

When to Use It

Reach for Observer when you can answer "yes" to any of these:

  • One state change should trigger several reactions. Cart updates trigger UI, analytics, save, recommendations. Order placement triggers email, inventory, accounting, fulfillment. The list grows.
  • The list of reactions should grow without modifying the source. New requirement, new listener — no edits to the publisher.
  • Different deployments need different reactions. Production has a real EmailNotifier; tests use a RecordingNotifier; the staging environment has both plus a SlackNotifier. Same publisher, different subscriber lists.
  • You want decoupling at the request level. Domain events fired during a request, picked up by handlers — same pattern, often shipped in frameworks as "events and listeners."

If exactly one consumer ever reacts to the change, just call it directly. Observer earns its keep when "exactly one" turns into "exactly one for now."

Pros and Cons

Pros

  • The publisher and subscribers don't know about each other directly.
  • Adding new subscribers is purely additive — no edits to the publisher or the other subscribers.
  • Subscriber lists can vary by environment, by tenant, by feature flag.
  • Each subscriber is small, focused, and individually testable.

Cons

  • The control flow becomes implicit. "What happens when an item is added?" is no longer answered by reading addItem — you have to know which observers are wired in.
  • Order of notification matters more than it should — and isn't usually visible.
  • A failing observer can break the chain, or worse, leave the system in a half-updated state. Decide your error semantics early.
  • Memory leaks lurk: forgotten subscriptions in long-lived processes hold references that prevent garbage collection.

Pro Tips

  • Make the notification payload immutable. If observers can mutate the event, they can also break each other. Pass a value, not a handle to mutable state.
  • Decide error semantics up front. Does a throwing observer abort the rest, or do the others still run? Both are valid choices — but choose, and document it.
  • Keep observer methods narrow. itemAdded should only know about the item-added event. Don't make a god observer that handles every kind of cart change.
  • Watch for cycles. If observer A's reaction triggers an event that observer B reacts to, which triggers something A reacts to — congratulations, you have a feedback loop. Test for these explicitly.
  • Always provide an unsubscribe. In long-lived processes (servers, single-page apps), forgotten subscriptions are a leak waiting to happen.

Relations with Other Patterns

  • Command is a natural fit on the receiving side: an Observer's reaction can be wrapping a Command and dropping it on a queue, so the work happens off the request thread.
  • State plays well with Observer: state transitions inside the Context can fire events that observers react to ("order moved to Shipped"), without the state objects knowing who's listening.
  • Mediator centralises bidirectional communication; Observer is one-way (subject → many observers). If your subscribers need to push state back into the publisher, Mediator may be the better fit.
  • Event Sourcing uses observer-style notification on every state change, but persists the events themselves as the source of truth.
  • Reactive Programming (RxJS, ReactiveX, observables in general) is Observer at scale, with operators for filtering, transforming, and composing event streams.

Final Tips

The Observer pattern is one of those things you've used a thousand times before knowing it had a name — every UI button click handler is one. The thing to learn isn't the mechanics; it's when to introduce the indirection in your own domain. Premature Observer turns simple "do X then do Y" code into a treasure hunt across the codebase. Earned Observer makes adding the seventh reaction to a domain event a five-minute change.

The smell that tells you it's time: when "do this then this then this" inside a method has grown to four steps and the next one is "and only sometimes do this fifth thing." That conditional is the moment subscribers start earning their keep.