Observer Pattern
Observer (Publish/Subscribe at object level) decouples event sources from handlers.
Introduction
Observer (Publish/Subscribe at object level) decouples event sources from handlers. A Subject maintains Observers and notifies them on state change. Modern variants: EventEmitter, domain events, RxJS, React subscriptions, Kafka topics. The pattern is how systems react without polling.
The story
A bank polled five downstream APIs every three seconds for loan status — under peak load the polling storm took down notification service. Domain events on state change cut API traffic 78% and gave customers instant updates.
The business problem
Tight coupling and polling create traffic storms and Open/Closed violations:
- Polling tax: downstream APIs hammered checking for changes that haven't happened.
- Edit-the-source: new reaction (audit, email) means editing publisher.
- Cascade coupling: publisher can't ship without coordinating every consumer.
- Missed reactions: teams forget to wire new side effects.
The problem teams faced
Observer fits when:
- Multiple subsystems react when one object's state changes.
- Adding reactions should not edit the source (Open/Closed).
- Polling exists because no push mechanism does.
- Reactions vary: email, analytics, audit, projection update.
Understanding the topic
Intent: One-to-many dependency — when subject changes, observers update automatically.
- Subject — attach/detach/notify observers.
- Observer — update(event) handler interface.
- Domain Event — modern variant: immutable fact emitted on change.
- Event Bus — decouples in-process or cross-service pub/sub.
Internal architecture
Domain event + subscribers:
class Order {private listeners = new Set<OrderListener>()subscribe(l: OrderListener) { this.listeners.add(l) }private emit(e: OrderEvent) { this.listeners.forEach(l => l.onOrderEvent(e)) }ship() {this.status = "SHIPPED"this.emit({ type: "SHIPPED", orderId: this.id, at: new Date() })}}
Visual explanation
Three diagrams cover notify flow, vs polling, and sync vs async:
Informative example
Before / after — polling to domain events:
// ❌ Poll five APIs every 3 secondssetInterval(async () => {for (const api of downstreamApis) {await api.checkLoanStatus(loanId) // 78% calls find no change}}, 3000)// ✅ Emit on change — subscribers react onceclass LoanApplication {approve() {this.status = "APPROVED"this.events.publish({ type: "LoanApproved", loanId: this.id })}}events.subscribe("LoanApproved", {notifyCustomer: (e) => emailService.send(e.loanId),updateBI: (e) => biPipeline.ingest(e),audit: (e) => auditLog.write(e),})
Execution workflow
Subscribe
Observers attach at init or runtime.
Real-world use
Node EventEmitter, Spring ApplicationEventPublisher, React useState re-renders, Redux dispatch, Kafka consumers, DDD domain events, GUI event listeners.
Production case study
Logistics "shipment delivered" reactions:
- Scenario: Five teams needed to react to delivery events.
- Decision: Domain event in-process for audit; Kafka for billing and BI.
- Outcome: Sixth reaction needed zero publisher changes.
Trade-offs
- Pro: Open/Closed for new reactions; eliminates polling.
- Pro: publisher decoupled from subscriber details.
- Con: notification order non-deterministic unless specified.
- Con: debugging event cascades harder than direct calls.
- Con: memory leaks if observers don't unsubscribe.
Decision framework
- Use when multiple independent reactions to same state change.
- Use when polling cost exceeds push infrastructure cost.
- Avoid when single synchronous caller — direct call simpler.
- Avoid Observer-for-everything without Command/State discipline.
Best practices
- Immutable domain events — facts, not commands.
- Unsubscribe on component dispose / request end.
- Async or outbox for slow subscribers — don't block publisher.
- Idempotent handlers for at-least-once delivery.
Anti-patterns to avoid
- Observer for everything — no trace of who started flow.
- Handlers mutating each other — circular cascades.
- Synchronous email send inside notify loop.
Common mistakes
- Memory leak from forgotten unsubscribe.
- Event handler throws — kills other subscribers unless isolated.
- Ordering assumptions across async handlers.
Debugging tips
- Correlation ID on every event — trace cascade across handlers.
- Log subscriber count and handler duration per event type.
Optimization strategies
- Batch events for high-frequency state changes (debounce UI).
- Separate hot path (in-process) from cold path (message bus).
Common misconceptions
- Observer ≠ Event Sourcing. Observer notifies; ES stores events as source of truth.
- Mediator coordinates; Observer broadcasts — different coupling.
- Kafka is Observer at network scale.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionObserver vs Pub/Sub vs Event Bus?+
Answer
Follow-up
2AdvancedQuestionSync vs async notification?+
Answer
Follow-up
3IntermediateQuestionMemory leak in Observer?+
Answer
Follow-up
4AdvancedQuestionObserver vs Mediator?+
Answer
Follow-up
Summary
You can replace polling with domain events, wire subscribers without editing publishers, and avoid cascade anti-patterns. Tell the loan-status polling storm story — if you can do that, you own Observer.