Behavioral Design Patterns: Observer, Strategy, and Command
Behavioral patterns define how objects communicate and distribute responsibility. Where creational patterns handle object construction, behavioral patterns focus on algorithms and the assignment of behavior at runtime. Observer, Strategy, and Command are the three you will reach for most often in real application code.
Published June 30, 2026The Gang of Four book catalogued 23 patterns, grouped into creational, structural, and behavioral categories. The creational patterns (Factory, Builder, Singleton) concern how objects come into existence. Behavioral patterns, covered here, concern how objects talk to each other and how responsibilities flow through a system at runtime. They are tools for reducing coupling between components — a direct application of the Open/Closed and Dependency Inversion principles.
Observer Pattern
The Observer pattern defines a one-to-many dependency. When a subject (also called a publisher or observable) changes state, all registered observers (subscribers) are notified automatically. Neither side needs to know the concrete type of the other: the subject knows only that observers have a update() method; observers know only that they can subscribe to a subject.
from abc import ABC, abstractmethod
from typing import List
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: object) -> None:
pass
class Subject:
def __init__(self):
self._observers: List[Observer] = []
def subscribe(self, observer: Observer) -> None:
self._observers.append(observer)
def unsubscribe(self, observer: Observer) -> None:
self._observers.remove(observer)
def notify(self, event: str, data: object = None) -> None:
for observer in self._observers:
observer.update(event, data)
class StockTicker(Subject):
def __init__(self, symbol: str):
super().__init__()
self.symbol = symbol
self._price = 0.0
@property
def price(self):
return self._price
@price.setter
def price(self, value: float):
self._price = value
self.notify("price_changed", {"symbol": self.symbol, "price": value})
class PriceAlert(Observer):
def __init__(self, threshold: float):
self.threshold = threshold
def update(self, event: str, data: object) -> None:
if event == "price_changed" and data["price"] > self.threshold:
print(f"Alert: {data['symbol']} crossed ${self.threshold:.2f}")
ticker = StockTicker("AAPL")
ticker.subscribe(PriceAlert(threshold=200.0))
ticker.price = 195.0 # no alert
ticker.price = 205.0 # triggers: Alert: AAPL crossed $200.00
Use Observer when:
- A change to one object requires updating others, and you do not know how many objects need updating at design time.
- You are building event systems, UI data bindings, or MVC architectures where the model notifies views.
- You want loose coupling between a producer of events and the consumers that react to them.
Be careful with observer chains: if observer A's update() triggers a state change that notifies observer B which triggers A again, you get an infinite loop. Also, synchronous notification blocks the subject until all observers complete — for slow observers, consider an async message queue instead.
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one in its own class, and makes them interchangeable. The client delegates to a strategy object rather than branching on algorithm type with conditionals. You can swap algorithms at runtime without touching the client.
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list:
pass
class QuickSort(SortStrategy):
def sort(self, data: list) -> list:
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
class MergeSort(SortStrategy):
def sort(self, data: list) -> list:
if len(data) <= 1:
return data
mid = len(data) // 2
left = self.sort(data[:mid])
right = self.sort(data[mid:])
return self._merge(left, right)
def _merge(self, left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
return result + left[i:] + right[j:]
class Sorter:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy):
self._strategy = strategy
def sort(self, data: list) -> list:
return self._strategy.sort(data)
sorter = Sorter(QuickSort())
print(sorter.sort([5, 3, 8, 1])) # [1, 3, 5, 8]
sorter.set_strategy(MergeSort())
print(sorter.sort([5, 3, 8, 1])) # [1, 3, 5, 8]
A practical example from payment systems: a PaymentProcessor class takes a PaymentStrategy. StripeStrategy, PayPalStrategy, and CryptoStrategy each implement the same charge(amount) interface. Adding a new payment method means writing a new strategy class, not modifying the processor. This is the Open/Closed Principle in direct action.
Use Strategy when you have multiple variants of an algorithm, when you want to eliminate a large conditional that switches between algorithms, or when an algorithm uses data the client should not know about. Avoid it for trivial cases where a simple function argument or a dict of callables achieves the same result with less indirection.
Command Pattern
The Command pattern encapsulates a request as an object. This lets you parameterize clients with different requests, queue or log requests, and implement undoable operations. The pattern has four participants: Command (interface), ConcreteCommand (wraps receiver + action), Receiver (the object that does the work), and Invoker (triggers commands, maintains history).
from abc import ABC, abstractmethod
from typing import List
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
class TextEditor:
def __init__(self):
self.content = ""
def insert(self, text: str, position: int) -> None:
self.content = self.content[:position] + text + self.content[position:]
def delete(self, position: int, length: int) -> None:
self.content = self.content[:position] + self.content[position + length:]
class InsertCommand(Command):
def __init__(self, editor: TextEditor, text: str, position: int):
self.editor = editor
self.text = text
self.position = position
def execute(self) -> None:
self.editor.insert(self.text, self.position)
def undo(self) -> None:
self.editor.delete(self.position, len(self.text))
class CommandHistory:
def __init__(self):
self._history: List[Command] = []
def execute(self, command: Command) -> None:
command.execute()
self._history.append(command)
def undo(self) -> None:
if self._history:
self._history.pop().undo()
editor = TextEditor()
history = CommandHistory()
history.execute(InsertCommand(editor, "Hello", 0))
history.execute(InsertCommand(editor, " World", 5))
print(editor.content) # Hello World
history.undo()
print(editor.content) # Hello
Use Command when:
- You need undo/redo functionality (editors, drawing apps, database transactions).
- You want to queue operations for deferred execution or background processing.
- You need an audit log: each executed command object can serialize itself.
- You are building a macro system where users record a sequence of actions to replay.
Command is overkill for a simple action that is never undone and never queued. In that case, a plain function or method call is clearer. The pattern shines when the operation lifecycle (create, queue, execute, undo, log) justifies an object. This pairs naturally with the principles in clean code naming: command classes should be named as imperative verbs (InsertCommand, DeleteRowCommand, SendEmailCommand) so their intent is immediately clear.