Design Patterns Lesson 5 of 6 ⏱️ 7 min read

Behavioral Patterns: Strategy, Observer & Command

Strategy, Observer, Command, State and Template Method — how objects collaborate, with runnable Python code.

Updated Oct 30, 2026

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.

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

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

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

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

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

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

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

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

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

Plaintext
[{'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/elif chains 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.
Finished this lesson?
Tick it off to track your Design Patterns progress.
Next lesson