Observer Pattern
The Observer pattern lets one object (subject) notify multiple dependents (observers) when its state changes. The subject maintains a subscriber list and calls each observer's update method. The subject doesn't need to know what observers do with the information—like a newsletter where subscribers automatically receive every new issue.
Quick Reference
Use When
- Changes in one object should trigger updates in others, but you don't know how many
- You want to decouple event sources from event handlers
- Building UI components that react to model/state changes
- Game entities subscribing to collision, input, or game state events
- Implementing undo/redo by observing state changes
Avoid When
- You have a fixed, small number of dependents that never change (just call methods directly)
- You need cross-service/cross-process communication (use Pub/Sub with a message broker instead)
- Observers need to transform or filter the event stream (use Reactive/RxJS instead)
- The notification chain becomes hard to trace (spaghetti callbacks)
- A simple callback or event handler suffices for your use case
The Analogy
A newsletter subscription: you sign up once and automatically receive every new issue. The publisher doesn't need to know who you are or what you do with the content. You can unsubscribe anytime, and the publisher keeps sending to everyone else.
The Problem
You have one object (the subject) whose state changes need to be communicated to multiple other objects (observers). Hard-coding notifications creates tight coupling—adding or removing observers requires modifying the subject. When observers are created and destroyed dynamically (UI components, game entities), tracking who to notify turns into bookkeeping you'll mess up.
The Solution
The subject maintains a list of observers and notifies them automatically when its state changes. Observers register themselves at runtime and can unsubscribe when done. The subject doesn't need to know the concrete types of its observers—just that they implement an update interface. This inverts the dependency: instead of the subject calling specific objects, objects register their interest.
Structure
Shows the static relationships between classes in the pattern.
Pattern Variants
Classic Observer (GoF)
Subject maintains observer list, calls update() on each. Observers know the subject and can query its state. Best for: tight integration where observers need context about the source.
EventEmitter / Event Target
Language-native event systems (Node EventEmitter, DOM addEventListener). Events are named, handlers receive event data. Best for: when you want string-based event names and multiple handler types.
Signals (Godot, Qt, Preact)
Typed, named events defined on objects. Connections are explicit (signal.connect(handler)). Lifecycle managed by the framework. Best for: game engines and reactive UI frameworks.
Reactive Streams (RxJS, Project Reactor)
Events as streams with operators (map, filter, debounce, merge). Handles backpressure and async sequences. Best for: complex event transformations, combining multiple sources, cancellation.
Implementations
Copy-paste examples in Python, JavaScript, and GDScript. Each includes validation and Director patterns.
import weakref
from typing import Callable, Any
from dataclasses import dataclass
# ===== Production-Safe Observer with Weak References =====
class Subject:
"""
Observer subject that prevents memory leaks using weak references.
Returns unsubscribe function for easy cleanup.
"""
def __init__(self):
# WeakSet: observers are garbage collected when no other refs exist
self._observers: weakref.WeakSet[Any] = weakref.WeakSet()
# For callable observers (functions/lambdas), we need strong refs
# because functions have no other references
self._callback_refs: set[Callable[[Any], None]] = set()
self._is_notifying = False # Re-entrancy guard
def subscribe(self, observer: Callable[[Any], None]) -> Callable[[], None]:
"""Subscribe and return an unsubscribe function."""
self._callback_refs.add(observer)
def unsubscribe():
self._callback_refs.discard(observer)
return unsubscribe
def subscribe_object(self, observer: Any) -> Callable[[], None]:
"""Subscribe an object with update() method. Uses weak ref."""
self._observers.add(observer)
def unsubscribe():
self._observers.discard(observer)
return unsubscribe
def notify(self, data: Any) -> None:
"""Notify all observers. Includes re-entrancy guard."""
if self._is_notifying:
return # Prevent infinite loops
self._is_notifying = True
try:
# Notify callback-based observers
for callback in list(self._callback_refs):
callback(data)
# Notify object-based observers (weak refs)
for observer in list(self._observers):
if hasattr(observer, 'update'):
observer.update(data)
finally:
self._is_notifying = False
# ===== Usage Example: Auth State =====
@dataclass
class AuthState:
user_id: str | None = None
is_authenticated: bool = False
class AuthStore(Subject):
def __init__(self):
super().__init__()
self._state = AuthState()
@property
def state(self) -> AuthState:
return self._state
def login(self, user_id: str) -> None:
self._state = AuthState(user_id=user_id, is_authenticated=True)
self.notify(self._state)
def logout(self) -> None:
self._state = AuthState()
self.notify(self._state)
# Usage with automatic cleanup
auth = AuthStore()
# Function-based observer
def on_auth_change(state: AuthState):
print(f"Auth changed: {state}")
unsubscribe = auth.subscribe(on_auth_change)
auth.login("user_123") # Prints: Auth changed: AuthState(user_id='user_123', ...)
# Clean up when done
unsubscribe()
auth.logout() # on_auth_change NOT called - already unsubscribedProduction Python implementation using WeakSet to prevent memory leaks for object observers. Returns unsubscribe functions for easy cleanup. Includes re-entrancy guard to prevent infinite notification loops. Separates callback vs object observers since functions need strong references.
// ===== Production-Safe Observer with AbortController =====
class Subject {
#observers = new Set();
#isNotifying = false;
/**
* Subscribe to updates. Returns unsubscribe function.
* @param {function} callback - Called with data on notify
* @param {AbortSignal} [signal] - Optional AbortSignal for auto-cleanup
*/
subscribe(callback, signal) {
// Handle already-aborted signals
if (signal?.aborted) {
return () => {}; // Return noop unsubscribe
}
this.#observers.add(callback);
// Auto-cleanup via AbortController (modern pattern)
if (signal) {
signal.addEventListener('abort', () => {
this.#observers.delete(callback);
}, { once: true }); // Prevent listener leak on long-lived signals
}
// Return manual unsubscribe function
return () => {
this.#observers.delete(callback);
};
}
notify(data) {
// Re-entrancy guard prevents infinite loops
if (this.#isNotifying) return;
this.#isNotifying = true;
try {
for (const observer of this.#observers) {
try {
observer(data);
} catch (err) {
console.error('Observer error:', err);
// Continue notifying others even if one fails
}
}
} finally {
this.#isNotifying = false;
}
}
get observerCount() {
return this.#observers.size;
}
}
// ===== Usage: Shopping Cart with Cleanup =====
class CartStore extends Subject {
#items = [];
add(item) {
this.#items = [...this.#items, item];
this.notify({ items: this.#items, action: 'add', item });
}
remove(id) {
const item = this.#items.find(i => i.id === id);
this.#items = this.#items.filter(i => i.id !== id);
this.notify({ items: this.#items, action: 'remove', item });
}
get items() {
return [...this.#items]; // Return copy to prevent mutation
}
}
// --- Example 1: Manual unsubscribe ---
const cart = new CartStore();
const unsubscribe = cart.subscribe((state) => {
console.log(`Cart updated: ${state.items.length} items`);
});
cart.add({ id: 1, name: 'Widget' }); // "Cart updated: 1 items"
unsubscribe();
cart.add({ id: 2, name: 'Gadget' }); // (no log - unsubscribed)
// --- Example 2: AbortController for bulk cleanup ---
const controller = new AbortController();
// Multiple subscriptions tied to same controller
cart.subscribe((s) => updateBadge(s.items.length), controller.signal);
cart.subscribe((s) => updateTotal(s.items), controller.signal);
cart.subscribe((s) => saveToLocalStorage(s.items), controller.signal);
// Later: unsubscribe ALL at once (e.g., component unmount)
controller.abort();
// --- Example 3: React integration pattern ---
function useCartSubscription(cart) {
const [items, setItems] = React.useState(cart.items);
React.useEffect(() => {
const unsubscribe = cart.subscribe((state) => {
setItems(state.items);
});
return unsubscribe; // Cleanup on unmount
}, [cart]);
return items;
}JavaScript implementation with AbortController integration for bulk cleanup (matches modern web APIs like fetch). Includes re-entrancy guard, error isolation (one failing observer doesn't break others), and immutable state copies. Shows React hook integration pattern with proper useEffect cleanup.
# ===== Godot Signals: The Built-in Observer Pattern =====
#
# Godot's signal system IS the Observer pattern, implemented in C++.
# Use signals as your primary approach—only use manual observers
# when you need custom behavior (filtering, batching, etc.)
# player.gd - The "Subject" (emits signals)
class_name Player
extends CharacterBody2D
# Declare signals (typed in Godot 4.x)
signal health_changed(new_health: int, max_health: int)
signal died
signal item_collected(item: Node2D)
@export var max_health: int = 100
var _health: int = max_health
var health: int:
get:
return _health
set(value):
var old_health := _health
_health = clamp(value, 0, max_health)
if _health != old_health:
health_changed.emit(_health, max_health)
if _health <= 0:
died.emit()
func take_damage(amount: int) -> void:
health -= amount
func heal(amount: int) -> void:
health += amount
func collect_item(item: Node2D) -> void:
item_collected.emit(item)
item.queue_free()
# ===== health_bar.gd - Observer (connects to signals) =====
extends ProgressBar
@export var player: Player
func _ready() -> void:
if player:
# Connect with explicit method (recommended for clarity)
player.health_changed.connect(_on_health_changed)
player.died.connect(_on_player_died)
# Initialize with current value
_on_health_changed(player.health, player.max_health)
func _exit_tree() -> void:
# Disconnect when removed from tree (prevents errors if player
# emits signal after this node is freed but before GC)
if player:
if player.health_changed.is_connected(_on_health_changed):
player.health_changed.disconnect(_on_health_changed)
if player.died.is_connected(_on_player_died):
player.died.disconnect(_on_player_died)
func _on_health_changed(new_health: int, max_health: int) -> void:
max_value = max_health
value = new_health
func _on_player_died() -> void:
modulate = Color.RED
# ===== One-shot connections (auto-disconnect after first emit) =====
extends Node
func _ready() -> void:
var player: Player = get_node("Player")
# CONNECT_ONE_SHOT: handler called once, then auto-disconnected
player.died.connect(_on_first_death, CONNECT_ONE_SHOT)
func _on_first_death() -> void:
print("Achievement unlocked: First Death")
# ===== Manual Observer (when you need custom control) =====
# Use this when signals don't fit: filtering, batching, priority ordering
class_name EventBus
extends Node
# Singleton event bus for decoupled communication
var _listeners: Dictionary = {} # event_name -> Array[Callable]
func subscribe(event_name: String, callback: Callable) -> Callable:
if not _listeners.has(event_name):
_listeners[event_name] = []
_listeners[event_name].append(callback)
# Return unsubscribe function
return func(): _listeners[event_name].erase(callback)
func emit_event(event_name: String, data: Variant = null) -> void:
if _listeners.has(event_name):
for callback in _listeners[event_name]:
callback.call(data)
# Usage:
# var unsub = EventBus.subscribe("player_scored", func(pts): score += pts)
# EventBus.emit_event("player_scored", 100)
# unsub.call() # UnsubscribeGDScript examples showing (1) Godot signals as the primary Observer implementation—they're optimized C++ and handle lifecycle automatically. Includes proper _exit_tree cleanup for edge cases. (2) CONNECT_ONE_SHOT for temporary listeners. (3) Manual EventBus pattern for when signals don't fit (cross-scene communication, filtering). Use signals first, manual observers only when needed.
Observer Pattern vs Pub/Sub
| Aspect | Observer Pattern | Pub/Sub |
|---|---|---|
| Coupling | Subject holds direct references to observers | Publisher and subscriber don't know each other |
| Intermediary | None—direct communication | Message broker (Redis, RabbitMQ, Kafka) |
| Scope | In-process, single application | Cross-process, distributed systems |
| Execution | Usually synchronous (blocking) | Asynchronous (fire-and-forget) |
| Use case | UI updates, local state sync | Microservices, event-driven architecture |
Real-World Examples
- React/Vue/Svelte: components re-render when observed state changes
- DOM events: addEventListener/removeEventListener for click, scroll, resize
- Node.js EventEmitter: on/off/emit for custom events
- Godot signals: connect/disconnect for node-to-node communication
- Redux/MobX: store notifies connected components on state update
- Stock tickers: multiple displays update when prices change
- Form validation: input fields notify the form of their validity state
Common Mistakes
Not providing an unsubscribe mechanism
class Subject:
def __init__(self):
self.observers = []
def subscribe(self, observer):
self.observers.append(observer) # No way to remove!
# Observer can never clean up → memory leak
subject.subscribe(my_handler)
# my_handler can't be garbage collected even when no longer neededFix: Always return an unsubscribe function from subscribe(). This matches modern patterns (RxJS, Redux, React hooks) and enables proper cleanup.
Observers modifying subject state during notification (re-entrancy)
class Counter(Subject):
def increment(self):
self.value += 1
self.notify(self.value)
# Observer that triggers another notification
def dangerous_observer(value):
if value < 10:
counter.increment() # Triggers notify() DURING notify() → stack overflow
counter.subscribe(dangerous_observer)
counter.increment() # 💥 Infinite recursionFix: Add a re-entrancy guard: set self._is_notifying = True during notify(), skip notification if already true. Or queue state changes and process after current notification completes.
Using Observer for cross-service communication
# BAD: Trying to use Observer across network boundaries
class OrderService:
def place_order(self, order):
# Direct observer call to another service... but what if it's down?
self.inventory_service.update(order) # Network call in disguise
self.email_service.update(order) # What if this fails?Fix: Use Pub/Sub with a message broker for cross-service communication. Brokers handle failures, retries, and ensure eventual delivery. Observer is for in-process notifications only.
Passing mutable state to observers
notify(data) {
for (const observer of this.observers) {
observer(this.state); // BAD: observers can mutate shared state
}
}
// Observer accidentally mutates
subscribe((state) => {
state.items.push(newItem); // Mutates subject's internal state!
});Fix: Pass immutable copies or frozen objects to observers. In JS: Object.freeze() or spread operator. In Python: dataclasses with frozen=True or return copies.
When to Use Observer Pattern
- When changes in one object require updating others, and you don't know how many objects need updating
- When an object should notify other objects without assumptions about who those objects are
- When you want to decouple the sender of a message from its receivers
- Building event-driven architectures or reactive UIs
- Implementing MVC/MVVM where the model notifies views of changes
- Game development: entities reacting to game state, collisions, or input events
Pitfalls to Avoid
- Memory leaks: Subject holds strong refs to observers → observer can't be garbage collected. FIX: Use weak references, or always return unsubscribe functions and call them in destructors/cleanup.
- Unexpected notification order: Observers receive events in registration order, which can cause subtle bugs. FIX: Design observers to be order-independent, or implement explicit priority levels.
- Cascade/recursive notifications: Observer updates subject, triggering another notify cycle → infinite loop. FIX: Add re-entrancy guard (isNotifying flag), or queue notifications and process after current cycle.
- Performance with many observers: Synchronous notification blocks until all observers complete. FIX: Batch notifications, use async/microtask scheduling, or debounce rapid updates.
- Silent failures: One observer throws, remaining observers don't get notified. FIX: Wrap observer calls in try/catch, log errors, continue to next observer.
- Debugging difficulty: Indirect control flow makes tracing "who triggered this?" hard. FIX: Add debug labels, use browser DevTools event breakpoints, or log with stack traces in dev mode.
Frequently Asked Questions
What is the difference between Observer and Pub/Sub?
Observer: subject holds direct references to observers and notifies them synchronously, so they're coupled at the object level. Pub/Sub: publishers and subscribers communicate through a message broker (Redis, RabbitMQ, Kafka) and don't know each other exists. Use Observer for in-process notifications (UI updates, local state). Use Pub/Sub for cross-service/distributed systems where you need resilience, async delivery, and full decoupling.
Observer vs EventEmitter vs Signals: which should I use?
Use what your platform provides: DOM addEventListener for browsers, EventEmitter for Node.js, Signals for Godot/Qt/Preact. These are Observer implementations with better ergonomics, lifecycle management, and performance (often implemented in C++/native code). Implement manual Observer only when you need custom behavior like filtering, batching, or priority ordering.
How do I prevent memory leaks with Observer?
Three strategies: (1) Always return an unsubscribe function from subscribe() and call it during cleanup (React useEffect return, Godot _exit_tree, Python __del__). (2) Use weak references (Python WeakSet, JS WeakRef) so observers can be garbage collected. (3) Use AbortController (JS) to bulk-unsubscribe multiple handlers at once. The subject should never prevent observers from being collected.
How do I prevent infinite notification loops?
Add a re-entrancy guard: set a flag (isNotifying) at the start of notify(), check it before notifying, clear it after. If notify() is called while already notifying, either skip (for idempotent updates) or queue the notification for after the current cycle completes. Also design observers to not modify subject state synchronously.
Can Observer notifications be async?
Yes, and often should be for performance. Options: (1) Notify using queueMicrotask/setTimeout/setImmediate to defer to next tick. (2) Batch multiple state changes, notify once at end of frame. (3) Use async notify() that awaits each observer (careful: slow observer blocks others). (4) Fire-and-forget with Promise.all() for parallel notification. Async prevents slow observers from blocking the subject.
When should I NOT use Observer?
Skip Observer when: (1) You have a fixed, small number of dependents, so just call methods directly. (2) The notification chain becomes hard to trace (symptom: "where did this event come from?"). (3) You need event transformation (map, filter, debounce), so use Reactive streams (RxJS). (4) Events cross service/process boundaries, so use Pub/Sub. (5) A simple callback parameter suffices. Observer adds indirection; only use it when you need dynamic subscription/unsubscription.
How do Godot signals relate to Observer pattern?
Godot signals ARE the Observer pattern, implemented in optimized C++. The node emitting the signal is the subject, connected nodes are observers. Signals handle lifecycle automatically (connections cleaned up when nodes are freed), support typed parameters, and integrate with the editor. Use signals as your default; implement manual observers only for cross-scene communication or custom event bus behavior.
How many observers is too many?
Synchronous notification of 10,000+ observers can cause noticeable frame drops. Mitigations: (1) Batch notifications (dirty flag, notify once per frame). (2) Use async notification (microtask/idle callback). (3) Implement observer coalescing (multiple rapid updates → one notification). (4) Consider if observers can poll instead of being pushed to. Profile your specific use case. 100 observers is usually fine, 10,000 needs optimization.