Skip to content

Observer Pattern

Observer Pattern is a pattern that implements Dependency Inversion. It supports Open-Closed Principle. It supports Interface Segregation Principle.

A publisher depends on a subscriber contract. Subscribers depend on event contracts. Publisher code does not know concrete subscriber classes.

Core Idea

Observer Pattern defines one to many notification flow. One subject publishes state change events. Many observers subscribe to those events. Each observer reacts through a stable interface.

This pattern decouples the producer from reaction logic. Producer code does not call concrete handlers directly. New handlers can be added with no producer edits.

Conceptual Overview

All examples in this page follow the shared context: Hypothetical Scenario.

The car platform has many reactions after one business action. A user profile update can trigger recommendation refresh, cost recalculation, and analytics updates. Direct calls from one service to many services create tight coupling. Observer Pattern replaces direct call chains with event subscription contracts.

The pattern has these roles.

  • Subject or Publisher: emits events
  • Observer or Subscriber: receives event notifications
  • Event Contract: defines payload shape and meaning

This split supports growth with lower coupling pressure.

Computing History

Observer appeared in object oriented design literature in the 1980s. The Gang of Four cataloged it in 1994 as a behavioral pattern. User interface toolkits and event systems used this pattern to detach state changes from screen updates.

Sources: Gamma et al. (1994)

The Problem It Solves

Observer Pattern solves change propagation problems.

  • one change must notify many independent modules
  • producer code should stay small and focused
  • new reactions should be added with low impact
  • notification behavior needs testable contracts

A direct call graph creates hard coupling. Producer imports each reaction module. Each new reaction forces producer edits and retesting. Observer Pattern removes this dependency fan out.

When to Use Observer

Use this pattern in these cases.

  • one business event has many downstream reactions
  • reaction list changes over time
  • producer should not own workflow policy for all reactions
  • modules need independent release cadence

Skip this pattern for strict request response workflows. Use direct orchestration in those workflows. Observer fits asynchronous notification style.

Generic Example: Python Domain Event Bus

from dataclasses import dataclass
from typing import Protocol


@dataclass(frozen=True)
class ProfileUpdated:
    profile_id: str
    budget_usd: int
    needs_family_car: bool


class ProfileUpdatedObserver(Protocol):
    def on_profile_updated(self, event: ProfileUpdated) -> None:
        raise NotImplementedError


class ProfileEventPublisher:
    def __init__(self) -> None:
        self._observers: list[ProfileUpdatedObserver] = []

    def subscribe(self, observer: ProfileUpdatedObserver) -> None:
        self._observers.append(observer)

    def publish_profile_updated(self, event: ProfileUpdated) -> None:
        for observer in self._observers:
            observer.on_profile_updated(event)


class RecommendationRefreshObserver(ProfileUpdatedObserver):
    def __init__(self, queue_port) -> None:
        self.queue_port = queue_port

    def on_profile_updated(self, event: ProfileUpdated) -> None:
        self.queue_port.enqueue(
            "recommendation.refresh",
            {
                "profile_id": event.profile_id,
                "budget_usd": event.budget_usd,
                "needs_family_car": event.needs_family_car,
            },
        )


class OwnershipCostRefreshObserver(ProfileUpdatedObserver):
    def __init__(self, calculator_port) -> None:
        self.calculator_port = calculator_port

    def on_profile_updated(self, event: ProfileUpdated) -> None:
        self.calculator_port.schedule_recompute(event.profile_id)

The publisher knows only the observer contract. Each observer owns one reaction concern.

Generic Example: TypeScript Observer Set

type ProfileUpdated = {
  profileId: string
  budgetUsd: number
  needsFamilyCar: boolean
}

interface ProfileUpdatedObserver {
  onProfileUpdated(event: ProfileUpdated): Promise<void>
}

class ProfileEventPublisher {
  private observers: ProfileUpdatedObserver[] = []

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

  async publish(event: ProfileUpdated): Promise<void> {
    for (const observer of this.observers) {
      await observer.onProfileUpdated(event)
    }
  }
}

class AnalyticsObserver implements ProfileUpdatedObserver {
  constructor(private readonly analyticsPort: { track: (name: string, payload: object) => Promise<void> }) {}

  async onProfileUpdated(event: ProfileUpdated): Promise<void> {
    await this.analyticsPort.track("profile_updated", {
      profileId: event.profileId,
      budgetUsd: event.budgetUsd,
    })
  }
}

Generic Example: ETL Pipeline with Observer Reactions

Observer Pattern can support ETL reactions after data landing. A pipeline publishes one batch_loaded event. Many observers consume that event.

from dataclasses import dataclass
from typing import Protocol


@dataclass(frozen=True)
class BatchLoaded:
    batch_id: str
    row_count: int
    source_name: str


class BatchLoadedObserver(Protocol):
    def on_batch_loaded(self, event: BatchLoaded) -> None:
        raise NotImplementedError


class BatchLoadedPublisher:
    def __init__(self) -> None:
        self._observers: list[BatchLoadedObserver] = []

    def subscribe(self, observer: BatchLoadedObserver) -> None:
        self._observers.append(observer)

    def publish(self, event: BatchLoaded) -> None:
        for observer in self._observers:
            observer.on_batch_loaded(event)


class QualityCheckObserver(BatchLoadedObserver):
    def __init__(self, quality_port) -> None:
        self.quality_port = quality_port

    def on_batch_loaded(self, event: BatchLoaded) -> None:
        self.quality_port.run_for_batch(event.batch_id)


class FeatureStoreObserver(BatchLoadedObserver):
    def __init__(self, feature_port) -> None:
        self.feature_port = feature_port

    def on_batch_loaded(self, event: BatchLoaded) -> None:
        self.feature_port.refresh_from_batch(event.batch_id)

This style keeps ETL stages decoupled. A new observer can be added for fraud models or alerting. Publisher code stays unchanged.

Dependency Injection and Substitution

Observer Pattern works best with dependency injection. Composition root builds publisher and subscribes concrete observers. Tests can pass fake observers through the same contract.

class SpyObserver(ProfileUpdatedObserver):
    def __init__(self) -> None:
        self.events: list[ProfileUpdated] = []

    def on_profile_updated(self, event: ProfileUpdated) -> None:
        self.events.append(event)


def test_profile_event_is_published_to_all_observers() -> None:
    publisher = ProfileEventPublisher()
    spy_a = SpyObserver()
    spy_b = SpyObserver()

    publisher.subscribe(spy_a)
    publisher.subscribe(spy_b)

    event = ProfileUpdated(profile_id="profile-42", budget_usd=25000, needs_family_car=True)
    publisher.publish_profile_updated(event)

    assert spy_a.events == [event]
    assert spy_b.events == [event]

This substitution follows Liskov. Any observer implementation that follows the contract can be injected.

Trade-offs and Failure Modes

Observer gives decoupling and growth flexibility. It adds delivery and visibility concerns.

Common risks:

  • hidden event chains that are hard to trace
  • duplicate processing after retries
  • ordering bugs between dependent observers
  • broad event payloads with unclear ownership

Control tactics:

  • version event contracts with schema checks
  • use idempotent consumers for repeated delivery
  • track event flow with correlation ids
  • keep observers focused on one reaction concern

This pattern links to Abstraction and Boundaries. Event contracts and observer interfaces define explicit boundaries.

This pattern links to Modularity and Composition. Each observer composes one focused reaction module.

This pattern links to Correctness and Testing. Contract based observers keep tests deterministic.

This pattern links to State and Data Modeling. Event payload shape and versioning protect data meaning across modules.

Practice Checklist

  • define narrow event contracts per business event
  • keep publisher free of concrete observer imports
  • keep one observer per reaction concern
  • wire observer registration in composition root
  • add idempotency for at least once delivery paths
  • add trace ids in event metadata
  • test publisher with fake observers
  • review event contract changes in design reviews

Written by: Pedro Guzmán

See References for complete APA-style bibliographic entries used on this page.