Observer Pattern: Event-Driven Design Guide
TL;DR
The Observer Pattern defines a one-to-many dependency between objects so that when one object (the subject) changes state, all its dependents (observers) are notified automatically. It’s the foundation for event-driven systems, UI frameworks, and publish-subscribe architectures.
Core Concept
What is the Observer Pattern?
The Observer Pattern is a behavioral design pattern that establishes a subscription mechanism. A subject (also called observable or publisher) maintains a list of observers (also called subscribers or listeners) and notifies them automatically when its state changes.
Think of it like a newsletter subscription: the newsletter publisher (subject) doesn’t need to know who its subscribers are or what they do with the content. Subscribers can join or leave at any time, and when a new issue is published, everyone on the list gets notified.
Key Components
Subject (Observable): The object being watched. It maintains a list of observers and provides methods to attach, detach, and notify them.
Observer (Subscriber): The object that wants to be notified of changes. It implements an update method that the subject calls when changes occur.
Concrete Subject: A specific implementation of the subject that holds the actual state being observed.
Concrete Observer: A specific implementation of the observer that defines how to respond to notifications.
Why It Matters
The Observer Pattern promotes loose coupling. The subject doesn’t need to know the concrete classes of its observers — it only knows they implement the observer interface. This means:
- You can add new observers without modifying the subject
- Observers can be added or removed at runtime
- The same subject can notify different types of observers
- Changes propagate automatically without explicit calls
Push vs. Pull Models
Push model: The subject sends detailed information about the change to observers (passes data in the notification).
Pull model: The subject only notifies that a change occurred; observers query the subject for details if needed.
Most implementations use a hybrid approach, passing minimal context in the notification while allowing observers to query for more details.
Visual Guide
Observer Pattern Structure
classDiagram
class Subject {
-observers: List~Observer~
+attach(observer: Observer)
+detach(observer: Observer)
+notify()
}
class Observer {
<<interface>>
+update(subject: Subject)
}
class ConcreteSubject {
-state: Any
+getState()
+setState(state)
}
class ConcreteObserverA {
+update(subject: Subject)
}
class ConcreteObserverB {
+update(subject: Subject)
}
Subject <|-- ConcreteSubject
Observer <|.. ConcreteObserverA
Observer <|.. ConcreteObserverB
Subject o-- Observer : notifies
The Subject maintains references to observers and notifies them of state changes. Observers implement a common interface to receive updates.
Observer Pattern Sequence
sequenceDiagram
participant Client
participant Subject
participant ObserverA
participant ObserverB
Client->>Subject: attach(ObserverA)
Client->>Subject: attach(ObserverB)
Client->>Subject: setState(newState)
Subject->>Subject: notify()
Subject->>ObserverA: update()
Subject->>ObserverB: update()
ObserverA->>Subject: getState()
ObserverB->>Subject: getState()
When the subject’s state changes, it automatically notifies all attached observers, who can then query the subject for details.
Examples
Example 1: Weather Station (Classic Observer Pattern)
Let’s build a weather station that notifies multiple displays when temperature changes.
from abc import ABC, abstractmethod
from typing import List
# Observer interface
class Observer(ABC):
@abstractmethod
def update(self, temperature: float) -> None:
pass
# Subject interface
class Subject(ABC):
@abstractmethod
def attach(self, observer: Observer) -> None:
pass
@abstractmethod
def detach(self, observer: Observer) -> None:
pass
@abstractmethod
def notify(self) -> None:
pass
# Concrete Subject
class WeatherStation(Subject):
def __init__(self):
self._observers: List[Observer] = []
self._temperature: float = 0.0
def attach(self, observer: Observer) -> None:
if observer not in self._observers:
self._observers.append(observer)
print(f"WeatherStation: Attached an observer.")
def detach(self, observer: Observer) -> None:
if observer in self._observers:
self._observers.remove(observer)
print(f"WeatherStation: Detached an observer.")
def notify(self) -> None:
print(f"WeatherStation: Notifying observers...")
for observer in self._observers:
observer.update(self._temperature)
def set_temperature(self, temperature: float) -> None:
print(f"\nWeatherStation: Temperature changed to {temperature}°C")
self._temperature = temperature
self.notify()
def get_temperature(self) -> float:
return self._temperature
# Concrete Observers
class PhoneDisplay(Observer):
def update(self, temperature: float) -> None:
print(f"PhoneDisplay: Temperature updated to {temperature}°C")
class WindowDisplay(Observer):
def update(self, temperature: float) -> None:
if temperature > 30:
print(f"WindowDisplay: HOT! {temperature}°C - Turn on AC!")
else:
print(f"WindowDisplay: Temperature is {temperature}°C")
class Logger(Observer):
def __init__(self):
self.log: List[float] = []
def update(self, temperature: float) -> None:
self.log.append(temperature)
print(f"Logger: Recorded {temperature}°C (Total readings: {len(self.log)})")
# Usage
weather_station = WeatherStation()
phone = PhoneDisplay()
window = WindowDisplay()
logger = Logger()
weather_station.attach(phone)
weather_station.attach(window)
weather_station.attach(logger)
weather_station.set_temperature(25.0)
weather_station.set_temperature(32.0)
weather_station.detach(window)
weather_station.set_temperature(28.0)
Expected Output:
WeatherStation: Attached an observer.
WeatherStation: Attached an observer.
WeatherStation: Attached an observer.
WeatherStation: Temperature changed to 25.0°C
WeatherStation: Notifying observers...
PhoneDisplay: Temperature updated to 25.0°C
WindowDisplay: Temperature is 25.0°C
Logger: Recorded 25.0°C (Total readings: 1)
WeatherStation: Temperature changed to 32.0°C
WeatherStation: Notifying observers...
PhoneDisplay: Temperature updated to 32.0°C
WindowDisplay: HOT! 32.0°C - Turn on AC!
Logger: Recorded 32.0°C (Total readings: 2)
WeatherStation: Detached an observer.
WeatherStation: Temperature changed to 28.0°C
WeatherStation: Notifying observers...
PhoneDisplay: Temperature updated to 28.0°C
Logger: Recorded 28.0°C (Total readings: 3)
Try it yourself: Add a StatisticsDisplay observer that calculates and displays the average temperature across all updates.
Example 2: Stock Market Tracker (Pull Model)
This example demonstrates the pull model where observers query the subject for data.
from abc import ABC, abstractmethod
from typing import List, Dict
class StockObserver(ABC):
@abstractmethod
def update(self) -> None:
"""Called when stock data changes"""
pass
class StockMarket:
def __init__(self):
self._observers: List[StockObserver] = []
self._stocks: Dict[str, float] = {}
def subscribe(self, observer: StockObserver) -> None:
self._observers.append(observer)
def unsubscribe(self, observer: StockObserver) -> None:
self._observers.remove(observer)
def notify_observers(self) -> None:
for observer in self._observers:
observer.update()
def update_stock(self, symbol: str, price: float) -> None:
self._stocks[symbol] = price
print(f"\nStockMarket: {symbol} updated to ${price}")
self.notify_observers()
def get_price(self, symbol: str) -> float:
return self._stocks.get(symbol, 0.0)
def get_all_stocks(self) -> Dict[str, float]:
return self._stocks.copy()
class InvestorApp(StockObserver):
def __init__(self, name: str, market: StockMarket, watching: List[str]):
self.name = name
self.market = market
self.watching = watching
self.market.subscribe(self)
def update(self) -> None:
print(f"{self.name}: Checking my stocks...")
for symbol in self.watching:
price = self.market.get_price(symbol)
if price > 0:
print(f" {symbol}: ${price}")
class TradingBot(StockObserver):
def __init__(self, market: StockMarket, buy_threshold: float):
self.market = market
self.buy_threshold = buy_threshold
self.market.subscribe(self)
def update(self) -> None:
stocks = self.market.get_all_stocks()
for symbol, price in stocks.items():
if price < self.buy_threshold:
print(f"TradingBot: BUY signal for {symbol} at ${price}!")
class NewsAlert(StockObserver):
def __init__(self, market: StockMarket, alert_threshold: float):
self.market = market
self.alert_threshold = alert_threshold
self.market.subscribe(self)
self.last_prices: Dict[str, float] = {}
def update(self) -> None:
stocks = self.market.get_all_stocks()
for symbol, price in stocks.items():
if symbol in self.last_prices:
change = abs(price - self.last_prices[symbol])
if change >= self.alert_threshold:
direction = "UP" if price > self.last_prices[symbol] else "DOWN"
print(f"NewsAlert: {symbol} moved {direction} by ${change:.2f}!")
self.last_prices[symbol] = price
# Usage
market = StockMarket()
investor1 = InvestorApp("Alice", market, ["AAPL", "GOOGL"])
investor2 = InvestorApp("Bob", market, ["TSLA"])
bot = TradingBot(market, 150.0)
news = NewsAlert(market, 10.0)
market.update_stock("AAPL", 175.50)
market.update_stock("GOOGL", 140.25)
market.update_stock("TSLA", 245.00)
market.update_stock("AAPL", 165.00) # Price drop triggers news alert
Expected Output:
StockMarket: AAPL updated to $175.5
Alice: Checking my stocks...
AAPL: $175.5
Bob: Checking my stocks...
StockMarket: GOOGL updated to $140.25
Alice: Checking my stocks...
AAPL: $175.5
GOOGL: $140.25
Bob: Checking my stocks...
TradingBot: BUY signal for GOOGL at $140.25!
StockMarket: TSLA updated to $245.0
Alice: Checking my stocks...
AAPL: $175.5
GOOGL: $140.25
Bob: Checking my stocks...
TSLA: $245.0
StockMarket: AAPL updated to $165.0
Alice: Checking my stocks...
AAPL: $165.0
GOOGL: $140.25
Bob: Checking my stocks...
TSLA: $245.0
NewsAlert: AAPL moved DOWN by $10.50!
Try it yourself: Implement a PortfolioTracker observer that maintains a portfolio of stocks and calculates total portfolio value on each update.
Java/C++ Notes
Java: Use the built-in java.util.Observer interface and java.util.Observable class (deprecated in Java 9+, but still illustrative). Modern Java uses PropertyChangeListener or reactive libraries like RxJava.
// Java example
interface Observer {
void update(Subject subject);
}
class ConcreteObserver implements Observer {
public void update(Subject subject) {
// Handle update
}
}
C++: Use function pointers, functors, or std::function for callbacks. Modern C++ often uses signals/slots libraries like Boost.Signals2 or Qt’s signal-slot mechanism.
// C++ example
class Observer {
public:
virtual void update(float temperature) = 0;
virtual ~Observer() = default;
};
class ConcreteObserver : public Observer {
public:
void update(float temperature) override {
// Handle update
}
};
Common Mistakes
1. Memory Leaks from Dangling Observers
Problem: Observers remain subscribed even after they’re no longer needed, preventing garbage collection.
# BAD: Observer never unsubscribes
class TempDisplay(Observer):
def __init__(self, station: WeatherStation):
station.attach(self) # Subscribes but never unsubscribes
def update(self, temp: float):
print(f"Temp: {temp}")
# Even if TempDisplay instance is deleted, it stays in the observer list
Solution: Always provide a way to unsubscribe, and use context managers or destructors to clean up.
# GOOD: Proper cleanup
class TempDisplay(Observer):
def __init__(self, station: WeatherStation):
self.station = station
station.attach(self)
def __del__(self):
self.station.detach(self)
def update(self, temp: float):
print(f"Temp: {temp}")
2. Notification Order Dependencies
Problem: Observers assume they’ll be notified in a specific order or that other observers have already processed the update.
# BAD: Observer B depends on Observer A's side effects
class ObserverA(Observer):
def update(self, temp: float):
shared_state.value = temp * 2 # Side effect
class ObserverB(Observer):
def update(self, temp: float):
# Assumes ObserverA already ran!
result = shared_state.value + 10
Solution: Each observer should be independent. If order matters, use explicit ordering or a different pattern (like Chain of Responsibility).
3. Modifying Observer List During Notification
Problem: Attaching or detaching observers while iterating through the observer list causes runtime errors or skipped notifications.
# BAD: Modifying list during iteration
class Subject:
def notify(self):
for observer in self._observers: # Iterating
observer.update(self)
# If update() calls detach(), we're modifying the list we're iterating!
Solution: Iterate over a copy of the observer list.
# GOOD: Iterate over a copy
class Subject:
def notify(self):
for observer in self._observers[:]: # Shallow copy
observer.update(self)
4. Circular Dependencies and Infinite Loops
Problem: Observer updates trigger subject changes, which trigger observer updates, creating an infinite loop.
# BAD: Circular update
class SubjectA(Subject):
def __init__(self, subject_b):
self.subject_b = subject_b
self.value = 0
def set_value(self, val):
self.value = val
self.notify()
class ObserverB(Observer):
def update(self, subject):
# This triggers another update!
subject.subject_b.set_value(subject.value + 1)
Solution: Use flags to prevent re-entrant updates or redesign to avoid circular dependencies.
# GOOD: Guard against re-entry
class SubjectA(Subject):
def __init__(self):
self.value = 0
self._notifying = False
def set_value(self, val):
if self._notifying:
return # Prevent re-entry
self.value = val
self._notifying = True
self.notify()
self._notifying = False
5. Exposing Too Much Subject State
Problem: Passing the entire subject to observers violates encapsulation and creates tight coupling.
# BAD: Observer has full access to subject internals
class Observer:
def update(self, subject):
# Observer can access everything!
subject._private_data = "modified"
subject._observers.clear() # Disaster!
Solution: Use the push model to send only necessary data, or provide controlled getter methods.
# GOOD: Pass only necessary data
class Observer:
def update(self, temperature: float, humidity: float):
# Observer only gets what it needs
print(f"Temp: {temperature}, Humidity: {humidity}")
Interview Tips
What Interviewers Look For
1. Recognize When to Use Observer Pattern
Interviewers often present scenarios and ask you to identify the appropriate pattern. Look for keywords:
- “Notify multiple objects when something changes”
- “Event-driven system”
- “Publish-subscribe”
- “One-to-many relationship”
- “Loosely coupled updates”
Example question: “Design a system where multiple UI components need to update when user data changes.” → This is Observer Pattern.
2. Explain Push vs. Pull Trade-offs
Be ready to discuss:
- Push model: More efficient if observers need all the data, but couples subject to observer data needs
- Pull model: More flexible, observers get only what they need, but requires more method calls
- Hybrid: Best of both — pass minimal context, let observers pull details
Interview answer template: “I’d use a hybrid approach: notify observers that a change occurred and pass the change type, then let observers query the subject for specific data they need. This balances efficiency with flexibility.”
3. Discuss Thread Safety
For senior positions, mention concurrency concerns:
- What happens if observers are added/removed during notification?
- What if multiple threads modify the subject simultaneously?
- Should notifications happen synchronously or asynchronously?
Strong answer: “I’d use a copy-on-write approach for the observer list and consider using a thread-safe queue for asynchronous notifications to prevent blocking the subject.”
4. Compare with Related Patterns
Be prepared to distinguish Observer from:
- Mediator: Centralizes communication vs. direct subject-to-observer notification
- Pub-Sub: Similar but typically has a message broker/event bus between publishers and subscribers
- Event Sourcing: Stores events as the source of truth vs. just notifying of state changes
5. Real-World Examples
Mention concrete implementations:
- GUI frameworks: Button clicks notify event listeners (Java Swing, Android)
- Reactive programming: RxJava, RxJS observables
- Model-View-Controller (MVC): Model notifies views of data changes
- Spreadsheets: Cells update when dependencies change
- Social media: Followers get notified of new posts
6. Code It Quickly
Practice implementing the basic structure in under 10 minutes:
- Define Observer interface with
update()method - Create Subject with
attach(),detach(),notify()methods - Implement one concrete observer
- Show usage: attach observer, change state, verify notification
7. Discuss Limitations
Show depth by mentioning when NOT to use Observer:
- Too many observers can cause performance issues (notification storm)
- Hard to debug — updates happen implicitly
- Can lead to memory leaks if observers aren’t properly detached
- Not suitable for complex event ordering requirements
Strong closing statement: “The Observer Pattern is powerful for decoupling, but I’d monitor the number of observers and consider using an event bus or message queue if the system grows to hundreds of observers or requires guaranteed delivery and ordering.”
Key Takeaways
-
Observer Pattern establishes a one-to-many dependency where the subject automatically notifies all observers when its state changes, enabling loose coupling and dynamic subscriptions.
-
Use Observer when multiple objects need to react to state changes in another object without tight coupling — common in event-driven systems, UI frameworks, and real-time data feeds.
-
Choose between push (subject sends data) and pull (observers query subject) models based on your needs: push is efficient when all observers need the same data, pull is flexible when observers need different subsets.
-
Always iterate over a copy of the observer list during notification to avoid errors when observers attach/detach themselves during updates, and implement proper cleanup to prevent memory leaks.
-
The pattern appears everywhere in software: GUI event listeners, reactive programming (RxJava/RxJS), MVC architecture, and pub-sub systems are all variations of the Observer Pattern.