Observer Pattern

intermediate 2-4 YOE lld design-pattern behavioral

The Observer pattern sets up a one-to-many relationship between objects. When one object (the subject) changes its state, all its dependents (observers) get notified and updated automatically.

Think of it like YouTube subscriptions. We subscribe to a channel. When the channel uploads a new video, every subscriber gets a notification. The channel doesn’t need to know who we are or what we do with the notification — it just broadcasts.

The Problem

Let’s say we have a stock price system. Multiple displays need to show the latest price — a dashboard, a mobile app, an alert system. Without Observer, we’d have to:

  • Make the stock object directly call each display (tight coupling)
  • Have each display constantly poll the stock for changes (wasteful)
  • Add new if-else branches every time we add a new display (violates Open/Closed)

The Observer pattern decouples all of this. The stock just says “hey, I changed” and everyone listening gets the update.

How It Works

Observer Pattern Structure
Subject (Publisher)
- observers: List<Observer>
- subscribe(observer)
- unsubscribe(observer)
- notify()
──notifies──▶
Observer (Subscriber)
- update(data)

Implementations:
- EmailAlert
- Dashboard
- MobileApp

The Subject keeps a list of observers. When something changes, it loops through the list and calls update() on each one. Observers can subscribe or unsubscribe at any time.

Push vs Pull Model

There are two ways the subject can share data with observers:

  • Push model — The subject sends the data directly in the update() call. Observers get everything whether they need it or not.
  • Pull model — The subject just says “I changed.” Observers then call back to the subject to get only the data they care about.

Push is simpler. Pull is more flexible when different observers need different pieces of data.

Implementation

from abc import ABC, abstractmethod

class Observer(ABC):
    @abstractmethod
    def update(self, news: str) -> None:
        pass

class NewsPublisher:
    def __init__(self):
        self._subscribers: list[Observer] = []
        self._latest_news = ""

    def subscribe(self, observer: Observer):
        self._subscribers.append(observer)

    def unsubscribe(self, observer: Observer):
        self._subscribers.remove(observer)

    def publish(self, news: str):
        self._latest_news = news
        self._notify()

    def _notify(self):
        for subscriber in self._subscribers:
            subscriber.update(self._latest_news)

class EmailSubscriber(Observer):
    def __init__(self, email: str):
        self.email = email

    def update(self, news: str):
        print(f"Email to {self.email}: {news}")

class AppSubscriber(Observer):
    def __init__(self, user_id: str):
        self.user_id = user_id

    def update(self, news: str):
        print(f"Push notification to {self.user_id}: {news}")

# Usage
publisher = NewsPublisher()
email_sub = EmailSubscriber("dev@example.com")
app_sub = AppSubscriber("user_42")

publisher.subscribe(email_sub)
publisher.subscribe(app_sub)
publisher.publish("Breaking: Observer pattern is awesome!")
# Email to dev@example.com: Breaking: Observer pattern is awesome!
# Push notification to user_42: Breaking: Observer pattern is awesome!
class NewsPublisher {
  constructor() {
    this.subscribers = [];
    this.latestNews = "";
  }

  subscribe(observer) {
    this.subscribers.push(observer);
  }

  unsubscribe(observer) {
    this.subscribers = this.subscribers.filter((s) => s !== observer);
  }

  publish(news) {
    this.latestNews = news;
    this.subscribers.forEach((sub) => sub.update(news));
  }
}

class EmailSubscriber {
  constructor(email) {
    this.email = email;
  }
  update(news) {
    console.log(`Email to ${this.email}: ${news}`);
  }
}

class AppSubscriber {
  constructor(userId) {
    this.userId = userId;
  }
  update(news) {
    console.log(`Push notification to ${this.userId}: ${news}`);
  }
}

// Usage
const publisher = new NewsPublisher();
const emailSub = new EmailSubscriber("dev@example.com");
const appSub = new AppSubscriber("user_42");

publisher.subscribe(emailSub);
publisher.subscribe(appSub);
publisher.publish("Breaking: Observer pattern is awesome!");
import java.util.ArrayList;
import java.util.List;

interface Observer {
    void update(String news);
}

class NewsPublisher {
    private List<Observer> subscribers = new ArrayList<>();
    private String latestNews;

    public void subscribe(Observer observer) {
        subscribers.add(observer);
    }

    public void unsubscribe(Observer observer) {
        subscribers.remove(observer);
    }

    public void publish(String news) {
        this.latestNews = news;
        for (Observer sub : subscribers) {
            sub.update(news);
        }
    }
}

class EmailSubscriber implements Observer {
    private String email;

    public EmailSubscriber(String email) { this.email = email; }

    public void update(String news) {
        System.out.println("Email to " + email + ": " + news);
    }
}

class AppSubscriber implements Observer {
    private String userId;

    public AppSubscriber(String userId) { this.userId = userId; }

    public void update(String news) {
        System.out.println("Push notification to " + userId + ": " + news);
    }
}

When to Use

  • Event systems — UI frameworks use this heavily (click handlers, state changes)
  • Pub/Sub messaging — message brokers like Kafka, RabbitMQ are Observer on steroids
  • Notification systems — email, SMS, push notifications when something happens
  • Data binding — React’s state updates, Angular’s change detection, MobX

When NOT to Use

  • When we only have one observer — overkill, just call it directly
  • When notification order matters a lot — Observer doesn’t guarantee order
  • When observers need to respond synchronously in sequence — can get messy
  • When the subject changes very frequently — notification storms can kill performance

Common Interview Questions

Q: How is Observer different from Pub/Sub? Observer is direct — the subject knows about observers and calls them. Pub/Sub has a middleman (message broker) so publishers and subscribers don’t know about each other at all.

Q: How do we avoid memory leaks? Always unsubscribe when an observer is done. In languages without garbage collection, dangling observer references are a classic memory leak.

In simple language, the Observer pattern is like a newsletter. We sign up, we get updates. We unsubscribe, we stop getting them. The publisher doesn’t care who’s reading — it just publishes. Clean, decoupled, and extensible.