Observer Pattern
Principle Link
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
Foundational Principle Links
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.