Observer Pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object (the subject/publisher) changes state, all its dependents (observers/subscribers) are notified and updated automatically. The publisher doesnβt know or care who is listening.
Why this matters: Anytime a problem says βnotify users when X happens,β βtrigger updates on state change,β or βmultiple components need to react to the same event,β Observer is the answer. Itβs the foundation of event-driven architecture at every scale.
Prerequisites
- SOLID Principles β Observer satisfies OCP (new listeners without modifying publisher)
- Strategy Pattern β similar structure, different purpose
Class Diagram
classDiagram
class EventPublisher {
-subscribers: Map~EventType, List~Subscriber~~
+subscribe(eventType, subscriber)
+unsubscribe(eventType, subscriber)
+notify(eventType, data)
}
class Subscriber {
<<interface>>
+onEvent(eventType, data)
}
class EmailNotifier {
+onEvent(eventType, data)
}
class SMSNotifier {
+onEvent(eventType, data)
}
class AuditLogger {
+onEvent(eventType, data)
}
EventPublisher --> Subscriber
Subscriber <|.. EmailNotifier
Subscriber <|.. SMSNotifier
Subscriber <|.. AuditLogger
Real-World Example: Payment Wallet Notifications
When a wallet transaction completes, multiple systems need to react: send a receipt, update the dashboard, log for auditing, trigger fraud checks. The wallet shouldnβt know about all these systems.
// Event types
public enum WalletEvent {
MONEY_ADDED, MONEY_DEDUCTED, TRANSFER_COMPLETED, LOW_BALANCE
}
// Subscriber interface
public interface WalletEventListener {
void onEvent(WalletEvent event, Transaction transaction);
}
// Event publisher (reusable component)
public class EventBus {
private final Map<WalletEvent, List<WalletEventListener>> listeners = new EnumMap<>(WalletEvent.class);
public void subscribe(WalletEvent event, WalletEventListener listener) {
listeners.computeIfAbsent(event, k -> new ArrayList<>()).add(listener);
}
public void unsubscribe(WalletEvent event, WalletEventListener listener) {
List<WalletEventListener> subs = listeners.get(event);
if (subs != null) subs.remove(listener);
}
public void publish(WalletEvent event, Transaction transaction) {
List<WalletEventListener> subs = listeners.getOrDefault(event, Collections.emptyList());
for (WalletEventListener listener : subs) {
listener.onEvent(event, transaction);
}
}
}
// Concrete listeners
public class ReceiptEmailer implements WalletEventListener {
@Override
public void onEvent(WalletEvent event, Transaction txn) {
String subject = switch (event) {
case MONEY_ADDED -> "Money added to wallet";
case MONEY_DEDUCTED -> "Payment processed";
case TRANSFER_COMPLETED -> "Transfer successful";
default -> "Wallet update";
};
emailService.send(txn.getUserEmail(), subject, buildReceiptBody(txn));
}
}
public class FraudDetector implements WalletEventListener {
@Override
public void onEvent(WalletEvent event, Transaction txn) {
if (txn.getAmount() > THRESHOLD || isUnusualLocation(txn)) {
flagForReview(txn);
}
}
}
public class BalanceAlertService implements WalletEventListener {
@Override
public void onEvent(WalletEvent event, Transaction txn) {
if (event == WalletEvent.MONEY_DEDUCTED && txn.getRemainingBalance() < MIN_BALANCE) {
pushNotification(txn.getUserId(), "Low balance alert");
}
}
}
// Usage: Wallet publishes events, doesn't know who listens
public class Wallet {
private final EventBus eventBus;
private double balance;
public Wallet(EventBus eventBus, double initialBalance) {
this.eventBus = eventBus;
this.balance = initialBalance;
}
public void addMoney(double amount, String source) {
balance += amount;
Transaction txn = new Transaction(amount, source, balance);
eventBus.publish(WalletEvent.MONEY_ADDED, txn);
}
public void deduct(double amount, String merchant) {
if (balance < amount) throw new InsufficientBalanceException();
balance -= amount;
Transaction txn = new Transaction(amount, merchant, balance);
eventBus.publish(WalletEvent.MONEY_DEDUCTED, txn);
}
}
from abc import ABC, abstractmethod
from enum import Enum, auto
from dataclasses import dataclass
class WalletEvent(Enum):
MONEY_ADDED = auto()
MONEY_DEDUCTED = auto()
TRANSFER_COMPLETED = auto()
LOW_BALANCE = auto()
@dataclass
class Transaction:
amount: float
counterparty: str
remaining_balance: float
user_id: str
class WalletEventListener(ABC):
@abstractmethod
def on_event(self, event: WalletEvent, txn: Transaction): ...
class EventBus:
def __init__(self):
self._listeners: dict[WalletEvent, list[WalletEventListener]] = {}
def subscribe(self, event: WalletEvent, listener: WalletEventListener):
self._listeners.setdefault(event, []).append(listener)
def unsubscribe(self, event: WalletEvent, listener: WalletEventListener):
if event in self._listeners:
self._listeners[event].remove(listener)
def publish(self, event: WalletEvent, txn: Transaction):
for listener in self._listeners.get(event, []):
listener.on_event(event, txn)
class ReceiptEmailer(WalletEventListener):
def on_event(self, event: WalletEvent, txn: Transaction):
subject = {
WalletEvent.MONEY_ADDED: "Money added to wallet",
WalletEvent.MONEY_DEDUCTED: "Payment processed",
}.get(event, "Wallet update")
send_email(txn.user_id, subject, build_receipt(txn))
class FraudDetector(WalletEventListener):
def on_event(self, event: WalletEvent, txn: Transaction):
if txn.amount > THRESHOLD:
flag_for_review(txn)
class Wallet:
def __init__(self, event_bus: EventBus, balance: float):
self._event_bus = event_bus
self._balance = balance
def add_money(self, amount: float, source: str):
self._balance += amount
txn = Transaction(amount, source, self._balance, self._user_id)
self._event_bus.publish(WalletEvent.MONEY_ADDED, txn)
def deduct(self, amount: float, merchant: str):
if self._balance < amount:
raise InsufficientBalanceError()
self._balance -= amount
txn = Transaction(amount, merchant, self._balance, self._user_id)
self._event_bus.publish(WalletEvent.MONEY_DEDUCTED, txn)
#include <string>
#include <vector>
#include <unordered_map>
#include <memory>
#include <algorithm>
enum class WalletEvent { MONEY_ADDED, MONEY_DEDUCTED, TRANSFER_COMPLETED };
struct Transaction {
double amount;
string counterparty;
double remainingBalance;
string userId;
};
class WalletEventListener {
public:
virtual ~WalletEventListener() = default;
virtual void onEvent(WalletEvent event, const Transaction& txn) = 0;
};
class EventBus {
unordered_map<int, vector<shared_ptr<WalletEventListener>>> listeners_;
public:
void subscribe(WalletEvent event, shared_ptr<WalletEventListener> listener) {
listeners_[static_cast<int>(event)].push_back(listener);
}
void publish(WalletEvent event, const Transaction& txn) {
int key = static_cast<int>(event);
if (listeners_.count(key)) {
for (auto& listener : listeners_[key]) {
listener->onEvent(event, txn);
}
}
}
};
class ReceiptEmailer : public WalletEventListener {
public:
void onEvent(WalletEvent event, const Transaction& txn) override {
// Send receipt email
}
};
class FraudDetector : public WalletEventListener {
public:
void onEvent(WalletEvent event, const Transaction& txn) override {
if (txn.amount > THRESHOLD) {
flagForReview(txn);
}
}
};
class Wallet {
shared_ptr<EventBus> eventBus_;
double balance_;
public:
Wallet(shared_ptr<EventBus> bus, double balance)
: eventBus_(bus), balance_(balance) {}
void addMoney(double amount, const string& source) {
balance_ += amount;
Transaction txn{amount, source, balance_, userId_};
eventBus_->publish(WalletEvent::MONEY_ADDED, txn);
}
void deduct(double amount, const string& merchant) {
if (balance_ < amount) throw InsufficientBalanceException();
balance_ -= amount;
Transaction txn{amount, merchant, balance_, userId_};
eventBus_->publish(WalletEvent::MONEY_DEDUCTED, txn);
}
};
Push vs Pull Models
| Model | How It Works | Use When |
|---|---|---|
| Push | Publisher sends full event data to observers | Observers always need the data; avoids extra calls |
| Pull | Publisher notifies; observers query for what they need | Different observers need different subsets of data |
The code above uses the push model (transaction data sent with the event). For pull, youβd pass a reference to the subject and let observers call getters.
Handling Slow Observers
If one observer is slow (e.g., sending emails), it blocks subsequent observers. Solutions:
- Async dispatch β Publish events to a queue; observers process independently
- Thread pool β Each observer gets its own thread
- Timeout + skip β If observer doesnβt respond in N ms, skip it
In a machine coding round, synchronous dispatch is usually fine. Mention async as an improvement.
When to Use vs When to Avoid
| Use Observer When | Avoid When |
|---|---|
| Multiple components react to the same event | Only one component needs to react (direct method call is simpler) |
| Publisher shouldnβt know about specific listeners | The dependency is obvious and fixed |
| New listeners will be added over time | The listener set is static and small |
| Event-driven requirements: βnotify whenβ¦β | Simple request-response flow |
| You need to decouple modules | Tight coupling is acceptable (same team, same module) |
Interview Questions
-
βHow do you prevent memory leaks with observers?β β Always provide an
unsubscribemechanism. In languages with GC, holding strong references to observers prevents collection. Use weak references or explicit cleanup. -
βWhat happens if an observer throws an exception?β β Depends on design: either catch and log (so other observers still fire), or propagate (fail-fast). In production, catch-and-log is safer.
-
βObserver vs Pub/Sub β whatβs the difference?β β Observer is in-process and synchronous. Pub/Sub typically involves a message broker (Kafka, RabbitMQ) for cross-service, asynchronous communication. Same concept, different scale.
-
βHow do you handle ordering of notifications?β β Observers fire in subscription order. If ordering matters, document it. If it shouldnβt matter, design observers to be independent.
See It in Action
| Music Player | Payment Wallet | Delivery Slot Booking |