Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 7 min read

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


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:

  1. Async dispatch – Publish events to a queue; observers process independently
  2. Thread pool – Each observer gets its own thread
  3. 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

  1. β€œHow do you prevent memory leaks with observers?” – Always provide an unsubscribe mechanism. In languages with GC, holding strong references to observers prevents collection. Use weak references or explicit cleanup.

  2. β€œ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.

  3. β€œ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.

  4. β€œ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

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access