# Observer

> Let an object announce its changes to a list of subscribers it does not know, so new reactions plug in without editing the source of the change.

- kind: pattern
- category: behavioral
- aliases: Publish-Subscribe, Event Listener, Dependents
- canonical: https://aipatterns.dev/design-patterns/observer
- inspired-by: https://refactoring.guru/design-patterns/observer

## Intent

Give an object (the *subject*) a list of subscribers and notify each of them when its state changes. The subject knows only that its subscribers implement one small interface. It does not know what they do with the news, how many there are, or who they are.

## Forces

- **One change** vs **many reactions**: a stock level drops, and the storefront, the purchasing team and the audit log all care, each for its own reason.
- **A stable core** vs **reactions that keep arriving**: every new reaction should not mean another line inside the method that made the change.
- **Loose coupling** vs **traceable control flow**: removing direct calls also removes the call site a reader would follow.
- **Long-lived subjects** vs **short-lived listeners**: a subject that holds references to its listeners keeps them alive until someone detaches them.
- **Synchronous delivery** vs **isolation**: a slow or throwing listener runs inside the subject's call and can stall or break it.

## Therefore

**Have the subject keep a list of objects that implement a notification interface, and call each of them after every relevant change.**

Subscribers register and unregister themselves at runtime. The subject builds one event value that describes the change and passes it to each one. In languages with first-class functions, a subscriber is usually a callback, and `subscribe` returns a function that removes it. Write an interface only when a subscriber reacts to several kinds of events.

## Structure

```mermaid
classDiagram
  class StockLedger {
    -List observers
    +subscribe(o) Unsubscribe
    +adjust(sku, delta) void
  }
  class StockObserver {
    <<interface>>
    +onStockChanged(change) void
  }
  class ReorderPlanner {
    -int reorderPoint
    +onStockChanged(change) void
  }
  class StorefrontAvailability {
    +onStockChanged(change) void
  }
  class AuditTrail {
    +onStockChanged(change) void
  }
  StockLedger o-- StockObserver : notifies
  StockObserver <|.. ReorderPlanner
  StockObserver <|.. StorefrontAvailability
  StockObserver <|.. AuditTrail
```

### Participants

- **Subject** (`StockLedger`): owns the state, keeps the subscriber list, and notifies after each change. It never checks a subscriber's concrete type.
- **Observer** (`StockObserver`): the notification interface. Keep it to one method per event kind.
- **Concrete observers** (`ReorderPlanner`, `StorefrontAvailability`, `AuditTrail`): each reacts to the event in its own way and knows nothing about the others.
- **Event** (`StockChange`): an immutable value that describes what changed, so observers don't need to call back into the subject.
- **Client**: wires observers to the subject and detaches them when they go away.

## Use it when

- A method ends with a growing list of "also tell X" calls that have nothing to do with its main job.
- Code in a lower layer must notify code in a higher layer without importing it, such as a domain model notifying a UI or a cache.
- The set of interested parties changes at runtime: plugins, open browser tabs, connected sockets.
- You need to add reactions (metrics, audit, cache invalidation) without editing or redeploying the code that causes the change.

## Do not use it when

- The events are plain announcements and a platform mechanism already covers them. Use `EventEmitter` or `EventTarget` in JavaScript, signals in a UI framework, C# events, or a channel in Go - a hand-rolled subject duplicates them and gets the edge cases wrong. Write your own subject only when you need filtering, ordering or in-process delivery rules those mechanisms don't have.
- There is exactly one reaction and it will not change. Call it directly; the call site documents the dependency.
- The order of reactions matters, or one reaction must finish before the next starts. Observers should not depend on each other's order. Write the sequence as plain code, or use [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md).
- You need delivery across processes, retries or persistence. Use a message queue or an outbox table. An in-memory observer list loses events on crash.
- You can't guarantee that every listener unsubscribes. A long-lived subject, such as a global store, holds a strong reference to each listener and leaks every component that forgot to unsubscribe. If you can't guarantee an unsubscribe path, use scoped subscriptions (framework effects with cleanup, `AbortSignal`, context cancellation) or weak references.

## Application grill

**Inspect first:** existing notification mechanisms (emitters, signals, streams, framework subscriptions) and the direct calls they would replace; whether any subscriber must not miss an event - that single requirement changes the infrastructure decision.

**Missing pieces:** in-process: the language's built-ins - nothing to buy. Cross-process or durable: a broker - Redis pub/sub for transient fan-out; SQS, SNS or Kafka when a subscriber must not miss events (that is a queue with consumer groups, not pub/sub).

**Ask before you build:**

- **Scale:** how many subscribers and what event rate - what happens when one subscriber lags (backpressure)?
- **Performance:** must delivery be synchronous within the request, or is eventual delivery acceptable per subscriber?
- **Reliability:** one failing subscriber must not fail the others - what isolates them (retries, dead letters)?
- **Budget:** managed broker versus in-process only - what tips the decision?
- **Implementation:** who owns event schemas and their versioning when payloads change?

## Implementation steps

1. Find the method that makes the change, and the calls to other objects at its end that only react to it.
2. Define an immutable event value with everything those reactions read. Don't make observers query the subject.
3. Define the observer interface, or a callback type, with one method per event kind.
4. Give the subject a collection of observers, a `subscribe` method that returns an unsubscribe handle, and a private `notify`.
5. Move each reaction into its own observer and subscribe it at the composition root.
6. Iterate over a copy of the list during `notify`, so an observer can unsubscribe while being notified.
7. Decide what happens when an observer throws: let it propagate, or catch it, log it and continue. Write that decision down in a test.
8. Pair every `subscribe` with an unsubscribe on the subscriber's teardown path.

## Example

A warehouse ledger tracks stock per SKU. When a level changes, the purchasing planner may reorder, the storefront hides sold-out items, and an audit trail records the move.

```ts title="stock.ts"
interface StockChange {
  sku: string;
  previous: number;
  onHand: number;
}

interface StockObserver {
  onStockChanged(change: StockChange): void;
}

class StockLedger {
  private readonly levels = new Map<string, number>();
  private readonly observers = new Set<StockObserver>();

  subscribe(observer: StockObserver): () => void {
    this.observers.add(observer);
    return () => this.observers.delete(observer);
  }

  adjust(sku: string, delta: number): void {
    const previous = this.levels.get(sku) ?? 0;
    const onHand = previous + delta;
    this.levels.set(sku, onHand);
    const change: StockChange = { sku, previous, onHand };
    // Copy so an observer can unsubscribe during notification.
    for (const observer of [...this.observers]) observer.onStockChanged(change);
  }
}

class ReorderPlanner implements StockObserver {
  constructor(private readonly reorderPoint: number) {}
  onStockChanged({ sku, previous, onHand }: StockChange): void {
    if (previous > this.reorderPoint && onHand <= this.reorderPoint) {
      console.log(`reorder ${sku}: ${onHand} left`);
    }
  }
}

class StorefrontAvailability implements StockObserver {
  readonly hidden = new Set<string>();
  onStockChanged({ sku, onHand }: StockChange): void {
    if (onHand <= 0) this.hidden.add(sku);
    else this.hidden.delete(sku);
  }
}

class AuditTrail implements StockObserver {
  readonly entries: string[] = [];
  onStockChanged({ sku, previous, onHand }: StockChange): void {
    this.entries.push(`${sku} ${previous}->${onHand}`);
  }
}

const ledger = new StockLedger();
const storefront = new StorefrontAvailability();
const audit = new AuditTrail();
ledger.subscribe(new ReorderPlanner(5));
ledger.subscribe(storefront);
const stopAudit = ledger.subscribe(audit);

ledger.adjust("KETTLE-01", 8);
ledger.adjust("KETTLE-01", -4); // reorder KETTLE-01: 4 left
stopAudit(); // detach on teardown, or the ledger keeps the trail alive
ledger.adjust("KETTLE-01", -4);
console.log([...storefront.hidden]); // [ 'KETTLE-01' ]
console.log(audit.entries); // [ 'KETTLE-01 0->8', 'KETTLE-01 8->4' ]
```

```python title="stock.py"
from dataclasses import dataclass
from typing import Callable, Protocol

@dataclass(frozen=True)
class StockChange:
    sku: str
    previous: int
    on_hand: int

class StockObserver(Protocol):
    def on_stock_changed(self, change: StockChange) -> None: ...

class StockLedger:
    def __init__(self) -> None:
        self._levels: dict[str, int] = {}
        self._observers: list[StockObserver] = []

    def subscribe(self, observer: StockObserver) -> Callable[[], None]:
        self._observers.append(observer)
        return lambda: self._observers.remove(observer)

    def adjust(self, sku: str, delta: int) -> None:
        previous = self._levels.get(sku, 0)
        self._levels[sku] = previous + delta
        change = StockChange(sku, previous, previous + delta)
        for observer in list(self._observers):  # copy: safe to unsubscribe mid-loop
            observer.on_stock_changed(change)

class ReorderPlanner:
    def __init__(self, reorder_point: int) -> None:
        self.reorder_point = reorder_point

    def on_stock_changed(self, change: StockChange) -> None:
        if change.previous > self.reorder_point >= change.on_hand:
            print(f"reorder {change.sku}: {change.on_hand} left")

class StorefrontAvailability:
    def __init__(self) -> None:
        self.hidden: set[str] = set()

    def on_stock_changed(self, change: StockChange) -> None:
        if change.on_hand <= 0:
            self.hidden.add(change.sku)
        else:
            self.hidden.discard(change.sku)

class AuditTrail:
    def __init__(self) -> None:
        self.entries: list[str] = []

    def on_stock_changed(self, change: StockChange) -> None:
        self.entries.append(f"{change.sku} {change.previous}->{change.on_hand}")

if __name__ == "__main__":
    ledger = StockLedger()
    storefront, audit = StorefrontAvailability(), AuditTrail()
    ledger.subscribe(ReorderPlanner(5))
    ledger.subscribe(storefront)
    stop_audit = ledger.subscribe(audit)

    ledger.adjust("KETTLE-01", 8)
    ledger.adjust("KETTLE-01", -4)  # reorder KETTLE-01: 4 left
    stop_audit()  # detach on teardown, or the ledger keeps the trail alive
    ledger.adjust("KETTLE-01", -4)
    print(storefront.hidden)  # {'KETTLE-01'}
    print(audit.entries)  # ['KETTLE-01 0->8', 'KETTLE-01 8->4']
```

```go title="stock.go"
package main

import "fmt"

type StockChange struct {
	SKU      string
	Previous int
	OnHand   int
}

type StockObserver interface {
	OnStockChanged(c StockChange)
}

type subscription struct {
	id       int
	observer StockObserver
}

type StockLedger struct {
	levels map[string]int
	subs   []subscription
	nextID int
}

func NewStockLedger() *StockLedger { return &StockLedger{levels: map[string]int{}} }

// Subscribe returns a function that removes the observer again.
func (l *StockLedger) Subscribe(o StockObserver) func() {
	l.nextID++
	id := l.nextID
	l.subs = append(l.subs, subscription{id, o})
	return func() {
		kept := l.subs[:0:0]
		for _, s := range l.subs {
			if s.id != id {
				kept = append(kept, s)
			}
		}
		l.subs = kept
	}
}

func (l *StockLedger) Adjust(sku string, delta int) {
	prev := l.levels[sku]
	l.levels[sku] = prev + delta
	change := StockChange{sku, prev, prev + delta}
	for _, s := range append([]subscription(nil), l.subs...) {
		s.observer.OnStockChanged(change)
	}
}

type ReorderPlanner struct{ ReorderPoint int }

func (r ReorderPlanner) OnStockChanged(c StockChange) {
	if c.Previous > r.ReorderPoint && c.OnHand <= r.ReorderPoint {
		fmt.Printf("reorder %s: %d left\n", c.SKU, c.OnHand)
	}
}

type StorefrontAvailability struct{ Hidden map[string]bool }

func (s *StorefrontAvailability) OnStockChanged(c StockChange) {
	s.Hidden[c.SKU] = c.OnHand <= 0
}

type AuditTrail struct{ Entries []string }

func (a *AuditTrail) OnStockChanged(c StockChange) {
	a.Entries = append(a.Entries, fmt.Sprintf("%s %d->%d", c.SKU, c.Previous, c.OnHand))
}

func main() {
	ledger := NewStockLedger()
	storefront := &StorefrontAvailability{Hidden: map[string]bool{}}
	audit := &AuditTrail{}
	ledger.Subscribe(ReorderPlanner{ReorderPoint: 5})
	ledger.Subscribe(storefront)
	stopAudit := ledger.Subscribe(audit)

	ledger.Adjust("KETTLE-01", 8)
	ledger.Adjust("KETTLE-01", -4) // reorder KETTLE-01: 4 left
	stopAudit()                    // detach on teardown, or the ledger keeps the trail alive
	ledger.Adjust("KETTLE-01", -4)
	fmt.Println(storefront.Hidden) // map[KETTLE-01:true]
	fmt.Println(audit.Entries)     // [KETTLE-01 0->8 KETTLE-01 8->4]
}
```

```java title="Stock.java"
import java.util.*;

public class Stock {
    record StockChange(String sku, int previous, int onHand) {}

    @FunctionalInterface
    interface StockObserver {
        void onStockChanged(StockChange change);
    }

    static final class StockLedger {
        private final Map<String, Integer> levels = new HashMap<>();
        private final List<StockObserver> observers = new ArrayList<>();

        Runnable subscribe(StockObserver observer) {
            observers.add(observer);
            return () -> observers.remove(observer);
        }

        void adjust(String sku, int delta) {
            int previous = levels.getOrDefault(sku, 0);
            levels.put(sku, previous + delta);
            var change = new StockChange(sku, previous, previous + delta);
            // Copy so an observer can unsubscribe during notification.
            for (StockObserver o : List.copyOf(observers)) o.onStockChanged(change);
        }
    }

    record ReorderPlanner(int reorderPoint) implements StockObserver {
        public void onStockChanged(StockChange c) {
            if (c.previous() > reorderPoint && c.onHand() <= reorderPoint) {
                System.out.println("reorder " + c.sku() + ": " + c.onHand() + " left");
            }
        }
    }

    static final class StorefrontAvailability implements StockObserver {
        final Set<String> hidden = new TreeSet<>();
        public void onStockChanged(StockChange c) {
            if (c.onHand() <= 0) hidden.add(c.sku()); else hidden.remove(c.sku());
        }
    }

    public static void main(String[] args) {
        var ledger = new StockLedger();
        var storefront = new StorefrontAvailability();
        List<String> audit = new ArrayList<>();
        ledger.subscribe(new ReorderPlanner(5));
        ledger.subscribe(storefront);
        // A lambda is an observer too.
        Runnable stopAudit = ledger.subscribe(
            c -> audit.add(c.sku() + " " + c.previous() + "->" + c.onHand()));

        ledger.adjust("KETTLE-01", 8);
        ledger.adjust("KETTLE-01", -4); // reorder KETTLE-01: 4 left
        stopAudit.run(); // detach on teardown, or the ledger keeps the lambda alive
        ledger.adjust("KETTLE-01", -4);
        System.out.println(storefront.hidden); // [KETTLE-01]
        System.out.println(audit); // [KETTLE-01 0->8, KETTLE-01 8->4]
    }
}
```

## Trade-offs

**Gains**
- New reactions are new code; the subject does not change.
- The subject depends on one interface, not on every consumer, so lower layers stay free of upper-layer imports.
- Subscriptions can change at runtime.
- Each observer is testable alone by feeding it an event value.

**Costs**
- Control flow is implicit. A reader of `adjust` can't see what runs after it without finding every `subscribe` call.
- Forgotten unsubscribes leak memory and cause "zombie" handlers that act on torn-down objects.
- Notification order is usually unspecified, and code that relies on it breaks silently.
- A throwing or slow observer runs on the subject's call stack and affects every caller of the subject.
- An observer that changes the subject during notification can trigger cascades or infinite loops.

## Relations

- [Mediator](https://aipatterns.dev/design-patterns/mediator.md) centralizes how a group of objects talk to each other. It is often implemented with observers, but a mediator knows its participants; a subject does not.
- [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) passes a request along until one handler deals with it, in a defined order. Observer delivers to every subscriber, in no order you should rely on.
- [Command](https://aipatterns.dev/design-patterns/command.md) turns a request into an object. Combine the two when events must be queued, replayed or logged before observers see them.
- [Singleton](https://aipatterns.dev/design-patterns/singleton.md) is how a global event bus usually starts. Avoid it: a global subject hides every dependency and holds every listener for the life of the process.

## Agent checklist

- [ ] You checked for an existing event mechanism in the platform or framework before writing a subject.
- [ ] Every `subscribe` has a matching unsubscribe on the subscriber's teardown path, verified by a test or a cleanup hook.
- [ ] The event is an immutable value with all the data observers need; observers don't call back into the subject.
- [ ] No observer depends on running before or after another one.
- [ ] Notification iterates over a copy of the list, and the behavior when an observer throws is decided and tested.
- [ ] The existing tests still pass, and each observer has a test that feeds it an event directly.
