Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Design Patterns
  3. Observer Pattern

Observer Pattern

BehavioralIntermediateAlso known as: Listener, Event-Subscriber, Dependents

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 ReferenceExamplesVariantsComparisonCommon MistakesFAQ

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

Observer Pattern: Observer Pattern class structure diagram

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 unsubscribed

Production 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()  # Unsubscribe

GDScript 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

AspectObserver PatternPub/Sub
CouplingSubject holds direct references to observersPublisher and subscriber don't know each other
IntermediaryNone—direct communicationMessage broker (Redis, RabbitMQ, Kafka)
ScopeIn-process, single applicationCross-process, distributed systems
ExecutionUsually synchronous (blocking)Asynchronous (fire-and-forget)
Use caseUI updates, local state syncMicroservices, 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

Example
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 needed

Fix: 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)

Example
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 recursion

Fix: 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

Example
# 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

Example
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.

Related Patterns

Strategy Pattern

Related Concepts

Python Classes Practice: self, super(), __repr__ vs __str__, inheritanceJavaScript Async/Await PracticeJavaScript DOM Manipulation PracticeJavaScript React Hooks PracticeJavaScript Classes Practice

Related Algorithms

BFS (Breadth-First Search)

Practice Observer Pattern

Learn by implementing. Our capstone exercises walk you through building this pattern step by step.

Available in Python and JavaScript.

← Back to Design Patterns
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.