Behavioral Patterns: Strategy, Observer & Command
Strategy, Observer, Command, State and Template Method — how objects collaborate, with runnable Python code.
Why behavioral patterns?
Behavioral patterns are about how objects communicate and how responsibilities are divided between them. They help replace long if/else chains, tightly coupled callbacks and duplicated workflows with flexible, testable designs.
| Pattern | Solves | Backend example |
|---|---|---|
| Strategy | Swapping algorithms at runtime | Pricing rules, shipping calculators, sort orders |
| Observer | Notifying many listeners when something happens | Domain events, webhooks, UI updates |
| Command | Turning a request into an object | Queue jobs, undo/redo, audit logs |
| State | Behaviour that depends on an object's state | Order or enrollment lifecycles |
| Template Method | A fixed algorithm with customisable steps | Import/export pipelines |
| Chain of Responsibility | Passing a request along a chain of handlers | Middleware, validation pipelines |
Strategy
The Strategy pattern defines a family of interchangeable algorithms and lets the caller pick one at runtime. In Python, a strategy can simply be a function.
def flat_fee(distance_km: float) -> int:
return 250
def per_km(distance_km: float) -> int:
return round(40 * distance_km)
def free_over_threshold(distance_km: float) -> int:
return 0
SHIPPING = {"flat": flat_fee, "distance": per_km, "promo": free_over_threshold}
def shipping_cost(method: str, distance_km: float) -> int:
return SHIPPING[method](distance_km)
for method in SHIPPING:
print(f"{method:<9} {shipping_cost(method, 12.5)}")
Output:
flat 250
distance 500
promo 0
Adding a strategy doesn't touch existing ones — a direct application of the Open/Closed Principle.
Observer
In the Observer pattern, a subject keeps a list of observers and notifies them when an event happens. The subject doesn't know what observers do — it only knows they're interested.
from collections import defaultdict
class EventBus:
def __init__(self):
self._listeners = defaultdict(list)
def subscribe(self, event: str, listener) -> None:
self._listeners[event].append(listener)
def publish(self, event: str, payload: dict) -> None:
for listener in self._listeners[event]:
listener(payload)
bus = EventBus()
bus.subscribe("student.registered", lambda p: print(f"email: welcome {p['name']}"))
bus.subscribe("student.registered", lambda p: print(f"analytics: +1 signup from {p['country']}"))
bus.subscribe("student.registered", lambda p: print(f"crm: create lead for {p['name']}"))
bus.publish("student.registered", {"name": "Ayesha", "country": "Pakistan"})
Output:
email: welcome Ayesha
analytics: +1 signup from Pakistan
crm: create lead for Ayesha
Registration code publishes one event; new features subscribe without changing it. Laravel events/listeners, Node's EventEmitter and browser addEventListener are all observers. Across services, the same idea becomes publish/subscribe with a message broker — see the Event-Driven Architecture tutorial.
Command
The Command pattern turns a request into an object with everything needed to execute it. Commands can be queued, logged, retried — and undone.
class Document:
def __init__(self):
self.text = ""
class AppendText:
def __init__(self, doc: Document, text: str):
self.doc, self.text = doc, text
def execute(self):
self.doc.text += self.text
def undo(self):
self.doc.text = self.doc.text[: -len(self.text)]
class Editor:
def __init__(self):
self.history: list = []
def run(self, command) -> None:
command.execute()
self.history.append(command)
def undo(self) -> None:
if self.history:
self.history.pop().undo()
doc, editor = Document(), Editor()
editor.run(AppendText(doc, "Hello"))
editor.run(AppendText(doc, ", students"))
print(doc.text)
editor.undo()
print(doc.text)
Output:
Hello, students
Hello
Queue jobs are commands: SendWelcomeEmail(user_id=42) is serialised, stored in Redis or RabbitMQ, and executed later by a worker.
State
The State pattern moves state-specific behaviour into separate state objects, replacing scattered if status == ... checks. Each state decides which transitions are allowed.
class Pending:
def pay(self, order): order.state = Paid(); return "payment received"
def cancel(self, order): order.state = Cancelled(); return "order cancelled"
def ship(self, order): return "cannot ship: not paid yet"
class Paid:
def pay(self, order): return "already paid"
def cancel(self, order): order.state = Cancelled(); return "cancelled, refund issued"
def ship(self, order): order.state = Shipped(); return "shipped"
class Shipped:
def pay(self, order): return "already paid"
def cancel(self, order): return "cannot cancel: already shipped"
def ship(self, order): return "already shipped"
class Cancelled:
def pay(self, order): return "cannot pay: cancelled"
def cancel(self, order): return "already cancelled"
def ship(self, order): return "cannot ship: cancelled"
class Order:
def __init__(self):
self.state = Pending()
def __getattr__(self, action): # delegate pay/cancel/ship to the current state
return lambda: getattr(self.state, action)(self)
order = Order()
for action in ("ship", "pay", "ship", "cancel"):
print(f"{action:<6} → {getattr(order, action)()} [{type(order.state).__name__}]")
Output:
ship → cannot ship: not paid yet [Pending]
pay → payment received [Paid]
ship → shipped [Shipped]
cancel → cannot cancel: already shipped [Shipped]
Invalid transitions are impossible by design, and adding a state (e.g. Refunded) means adding a class. This is a finite state machine — a model used for orders, payments, enrollments and workflows.
Template Method
Template Method defines the skeleton of an algorithm in a base class and lets subclasses fill in specific steps.
import csv
import io
import json
from abc import ABC, abstractmethod
class Importer(ABC):
def run(self, raw: str) -> list[dict]: # the fixed algorithm
rows = self.parse(raw)
valid = [r for r in rows if r.get("email")]
return [{**r, "email": r["email"].lower()} for r in valid]
@abstractmethod
def parse(self, raw: str) -> list[dict]: ... # the step that varies
class CsvImporter(Importer):
def parse(self, raw):
return list(csv.DictReader(io.StringIO(raw)))
class JsonImporter(Importer):
def parse(self, raw):
return json.loads(raw)
print(CsvImporter().run("name,email\nAli,ALI@EXAMPLE.COM\nNo Email,\n"))
print(JsonImporter().run('[{"name": "Sara", "email": "Sara@Example.com"}]'))
Output:
[{'name': 'Ali', 'email': 'ali@example.com'}]
[{'name': 'Sara', 'email': 'sara@example.com'}]
Validation and normalisation are written once; each format only implements parsing. Template Method uses inheritance; Strategy achieves similar flexibility with composition — usually the more flexible choice.
Chain of Responsibility
A request passes through a chain of handlers; each can handle it, modify it, or pass it on. HTTP middleware is the best-known example: authentication → rate limiting → CSRF check → controller. Any handler can stop the chain (e.g. return 401 Unauthorized).
Other behavioral patterns
- Iterator — sequential access without exposing internals (Python generators).
- Mediator — objects communicate through a central mediator instead of directly.
- Memento — capture and restore an object's state (save points, undo).
- Visitor — add operations to object structures without changing their classes (compilers, AST tools).
Common mistakes
- Observers with hidden ordering dependencies — listeners should be independent.
- Synchronous observers doing slow work (sending email inline) — queue it instead.
- Huge
if/elifchains on type or status — a sign that Strategy or State would help. - Deep Template Method hierarchies — prefer composing strategies.
Key takeaways
- Strategy swaps algorithms; in Python a function is often enough.
- Observer decouples event producers from listeners.
- Command turns requests into objects — the basis of queues and undo.
- State replaces status conditionals with state objects and safe transitions.
- Template Method fixes an algorithm's skeleton and customises steps.