backend / theory foundations / 07_behavioral_patterns.md

Behavioral Design Patterns (GoF)

9 interview angles 14 min read source

Behavioral Design Patterns (GoF)

The eleven GoF behavioral patterns address how objects communicate and how responsibilities are distributed. They’re about algorithms, control flow, and the assignment of behavior.

Reference: refactoring.guru / Design Patterns — illustrated explanations of every GoF pattern; cross-reference each pattern below against the canonical descriptions there.

The eleven behavioral patterns:

Pattern Intent refactoring.guru
Chain of Responsibility Pass request along a chain of handlers https://refactoring.guru/design-patterns/chain-of-responsibility
Command Encapsulate a request as an object https://refactoring.guru/design-patterns/command
Interpreter Define a grammar; interpret sentences (Less prominent on refactoring.guru)
Iterator Sequential access to a collection https://refactoring.guru/design-patterns/iterator
Mediator Object encapsulating object interactions https://refactoring.guru/design-patterns/mediator
Memento Capture and restore object state https://refactoring.guru/design-patterns/memento
Observer Subscribers notified of subject changes https://refactoring.guru/design-patterns/observer
State Alter behavior when internal state changes https://refactoring.guru/design-patterns/state
Strategy Interchangeable algorithms https://refactoring.guru/design-patterns/strategy
Template Method Skeleton of an algorithm with overridable steps https://refactoring.guru/design-patterns/template-method
Visitor Separate an algorithm from the objects it operates on https://refactoring.guru/design-patterns/visitor

For creational see 05_creational_patterns.md; for structural 06_structural_patterns.md.

Chain of Responsibility

Intent: Avoid coupling sender to receiver by giving multiple objects a chance to handle a request. Chain receivers; pass the request along until one handles it. Reference: https://refactoring.guru/design-patterns/chain-of-responsibility

Problem: A request might be handled by one of several objects; you don’t want the sender to know which. Each handler may decide to handle it, ignore it, or pass it to the next.

from abc import ABC, abstractmethod
from typing import Optional

class Handler(ABC):
    def __init__(self):
        self._next: Optional[Handler] = None

    def set_next(self, handler: "Handler") -> "Handler":
        self._next = handler
        return handler                                # for chaining

    def handle(self, request):
        if self._next:
            return self._next.handle(request)
        return None

class AuthHandler(Handler):
    def handle(self, request):
        if not request.get("auth"):
            return "401 Unauthorized"
        return super().handle(request)

class RateLimitHandler(Handler):
    def handle(self, request):
        if request.get("requests_per_minute", 0) > 100:
            return "429 Too Many Requests"
        return super().handle(request)

class CacheHandler(Handler):
    def handle(self, request):
        if cached := request.get("_cached"):
            return f"Cache hit: {cached}"
        return super().handle(request)

class HandlerEnd(Handler):
    def handle(self, request):
        return "200 OK — handled by application"

# Build the chain
chain = AuthHandler()
chain.set_next(RateLimitHandler()).set_next(CacheHandler()).set_next(HandlerEnd())

print(chain.handle({"auth": "token", "requests_per_minute": 50}))  # "200 OK"
print(chain.handle({"requests_per_minute": 50}))                    # "401 Unauthorized"

Real-world: HTTP middleware stacks (Django, Flask, Express), event bubbling in UIs, logging filters.

When to use: variable / configurable processing pipelines; multiple handlers eligible; loose coupling between sender and handler.

Pitfalls: chain can be incomplete (no handler matches); ordering matters; debugging traversal through long chains.

Command

Intent: Encapsulate a request as an object, letting you parameterize clients with different requests, queue or log requests, and support undoable operations. Reference: https://refactoring.guru/design-patterns/command

Problem: You want to decouple the invoker from the receiver. The invoker says “do this command”; doesn’t know how.

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self): ...
    @abstractmethod
    def undo(self): ...

class Light:
    def __init__(self):
        self.on = False
    def turn_on(self): self.on = True
    def turn_off(self): self.on = False

class TurnOnCommand(Command):
    def __init__(self, light): self.light = light
    def execute(self): self.light.turn_on()
    def undo(self): self.light.turn_off()

class TurnOffCommand(Command):
    def __init__(self, light): self.light = light
    def execute(self): self.light.turn_off()
    def undo(self): self.light.turn_on()

class RemoteControl:
    def __init__(self):
        self.history: list[Command] = []
    def press(self, command: Command):
        command.execute()
        self.history.append(command)
    def undo_last(self):
        if self.history:
            self.history.pop().undo()

light = Light()
remote = RemoteControl()
remote.press(TurnOnCommand(light))      # light on
remote.press(TurnOffCommand(light))      # light off
remote.undo_last()                        # light on again

When to use: undo/redo, command queues, macro recording, task scheduling, GUI buttons mapped to actions.

Pitfalls: too many command classes for trivial operations; pure ceremony for one-liners. Pythonic alternative: a callable (function or lambda) often suffices.

Interpreter

Intent: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language. Reference: (Less commonly applied; refactoring.guru notes its decline in favor of parser generators.)

Problem: You need to evaluate expressions in a domain-specific language (math expressions, search queries, filter rules).

from abc import ABC, abstractmethod

class Expression(ABC):
    @abstractmethod
    def interpret(self, context: dict) -> int: ...

class Number(Expression):
    def __init__(self, value): self.value = value
    def interpret(self, context): return self.value

class Variable(Expression):
    def __init__(self, name): self.name = name
    def interpret(self, context): return context[self.name]

class Add(Expression):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    def interpret(self, context):
        return self.left.interpret(context) + self.right.interpret(context)

class Multiply(Expression):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    def interpret(self, context):
        return self.left.interpret(context) * self.right.interpret(context)

# Build AST: (x + 5) * 3
expr = Multiply(Add(Variable("x"), Number(5)), Number(3))
print(expr.interpret({"x": 10}))     # (10 + 5) * 3 = 45

When to use: small, stable DSLs (filter expressions, simple query languages).

Real-world: Django Q-objects, SQLAlchemy expression language, math expression evaluators.

Pitfalls: complex grammars are better handled by parser generators (PLY, lark) or libraries; rolling your own interpreter doesn’t scale.

Iterator

Intent: Provide a way to access elements of a collection sequentially without exposing its underlying representation. Reference: https://refactoring.guru/design-patterns/iterator

Problem: Different collections (lists, trees, graphs) have different traversal logic. Clients shouldn’t depend on a collection’s internal structure.

In Python, iterators are first-class. The pattern is built into the language via __iter__ and __next__.

class WordCollection:
    def __init__(self):
        self._words: list[str] = []
    def add(self, word):
        self._words.append(word)
    def __iter__(self):
        return WordIterator(self)

class WordIterator:
    def __init__(self, collection):
        self._collection = collection
        self._index = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self._index >= len(self._collection._words):
            raise StopIteration
        word = self._collection._words[self._index]
        self._index += 1
        return word

words = WordCollection()
for w in ["apple", "banana", "cherry"]:
    words.add(w)
for w in words:
    print(w)

Pythonic shortcut — generators ARE iterators:

class WordCollection:
    def __init__(self):
        self._words = []
    def add(self, word):
        self._words.append(word)
    def __iter__(self):
        yield from self._words

When to use: custom traversal (in-order tree walk, breadth-first graph), exposing iteration without exposing internals.

Real-world: every Python for loop, itertools, ORM queryset iteration, file line-by-line reading.

Pitfalls: forgetting StopIteration, stateful iterators that can’t be re-iterated.

See ../02_python_core/08_class_iterator_generator.md and ../02_python_core/10_iterator_vs_generator.md.

Mediator

Intent: Define an object that encapsulates how a set of objects interact. Promotes loose coupling by keeping objects from referring to each other explicitly. Reference: https://refactoring.guru/design-patterns/mediator

Problem: Many objects need to talk to each other. Direct references between all pairs produce a tangle (N×N coupling). A mediator centralizes the routing.

from abc import ABC, abstractmethod

class Mediator(ABC):
    @abstractmethod
    def notify(self, sender, event): ...

class ChatRoom(Mediator):
    def __init__(self):
        self.users: list[User] = []
    def register(self, user):
        self.users.append(user)
        user.mediator = self
    def notify(self, sender, event):
        for user in self.users:
            if user is not sender:
                user.receive(sender.name, event)

class User:
    def __init__(self, name):
        self.name = name
        self.mediator: Mediator | None = None
    def send(self, message):
        print(f"{self.name} sends: {message}")
        self.mediator.notify(self, message)
    def receive(self, sender_name, message):
        print(f"{self.name} receives from {sender_name}: {message}")

room = ChatRoom()
alice = User("Alice"); room.register(alice)
bob = User("Bob"); room.register(bob)
carol = User("Carol"); room.register(carol)

alice.send("Hi everyone!")

Users don’t reference each other. They reference the mediator.

When to use: many-to-many object interactions; UI dialogs where buttons / text fields / dropdowns affect each other; air traffic control style systems.

Pitfalls: the mediator can become a god object holding ALL the routing logic. Split mediators if it grows too large.

Real-world: message brokers (Kafka, RabbitMQ — distributed mediators), event buses, MVC controllers acting as mediators between models and views.

Memento

Intent: Without violating encapsulation, capture and externalize an object’s internal state so the object can be restored to this state later. Reference: https://refactoring.guru/design-patterns/memento

Problem: You want undo / snapshots. Exposing the object’s internal state to enable saving breaks encapsulation.

from copy import deepcopy

class EditorMemento:
    def __init__(self, content: str):
        self._content = content       # private to the editor

    def get_content(self):
        return self._content

class Editor:
    def __init__(self):
        self.content = ""

    def write(self, text):
        self.content += text

    def save(self) -> EditorMemento:
        return EditorMemento(self.content)

    def restore(self, memento: EditorMemento):
        self.content = memento.get_content()

class History:
    def __init__(self):
        self.stack: list[EditorMemento] = []
    def push(self, memento):
        self.stack.append(memento)
    def pop(self):
        return self.stack.pop()

editor = Editor()
history = History()
editor.write("Hello")
history.push(editor.save())
editor.write(" world")
history.push(editor.save())
editor.write("!")
print(editor.content)              # "Hello world!"
editor.restore(history.pop())       # back to "Hello world"
print(editor.content)              # "Hello world"

When to use: undo/redo, snapshots, transactional changes.

Pitfalls: large state → large mementos → memory growth. Use diff-based memento for large objects, or limit history size.

Real-world: text editor undo, database savepoints, Git commit history (conceptually).

Observer

Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Reference: https://refactoring.guru/design-patterns/observer

Problem: Multiple parts of a system need to react to events in another part. Hard-coding the dependency creates tight coupling.

from abc import ABC, abstractmethod

class Subject:
    def __init__(self):
        self._observers: list[Observer] = []
        self._state = None

    def attach(self, observer): self._observers.append(observer)
    def detach(self, observer): self._observers.remove(observer)

    @property
    def state(self): return self._state

    @state.setter
    def state(self, value):
        self._state = value
        self._notify()

    def _notify(self):
        for o in self._observers:
            o.update(self)

class Observer(ABC):
    @abstractmethod
    def update(self, subject): ...

class EmailNotifier(Observer):
    def update(self, subject):
        print(f"Email: state changed to {subject.state}")

class Logger(Observer):
    def update(self, subject):
        print(f"Log: state = {subject.state}")

subject = Subject()
subject.attach(EmailNotifier())
subject.attach(Logger())
subject.state = "active"

When to use: event systems, model-view in UIs, pub/sub patterns, reactive programming.

Real-world: Django signals, JS DOM event listeners, RxPy / RxJS, Redis pub/sub.

Pitfalls:

  • Memory leaks if observers aren’t detached.
  • Ordering of observer notification.
  • Cascading updates: observer A’s update triggers subject B, which has its own observers…
  • Synchronous notification blocks the subject.

State

Intent: Allow an object to alter its behavior when its internal state changes. The object appears to change its class. Reference: https://refactoring.guru/design-patterns/state

Problem: A class has many if state == "X" branches in methods. Adding states requires touching every method.

from abc import ABC, abstractmethod

class State(ABC):
    @abstractmethod
    def insert_coin(self, machine): ...
    @abstractmethod
    def select_product(self, machine): ...
    @abstractmethod
    def dispense(self, machine): ...

class IdleState(State):
    def insert_coin(self, machine):
        print("Coin inserted")
        machine.state = HasCoinState()
    def select_product(self, machine): print("Insert coin first")
    def dispense(self, machine): print("Insert coin first")

class HasCoinState(State):
    def insert_coin(self, machine): print("Already has coin")
    def select_product(self, machine):
        print("Product selected")
        machine.state = DispensingState()
    def dispense(self, machine): print("Select product first")

class DispensingState(State):
    def insert_coin(self, machine): print("Wait, dispensing")
    def select_product(self, machine): print("Wait, dispensing")
    def dispense(self, machine):
        print("Product dispensed")
        machine.state = IdleState()

class VendingMachine:
    def __init__(self):
        self.state: State = IdleState()
    def insert_coin(self): self.state.insert_coin(self)
    def select_product(self): self.state.select_product(self)
    def dispense(self): self.state.dispense(self)

vm = VendingMachine()
vm.insert_coin()
vm.select_product()
vm.dispense()

State vs Strategy:

  • Strategy: client picks an algorithm; algorithms are interchangeable.
  • State: the object changes its own state based on internal events; states drive transitions.

Real-world: state machines (TCP connection lifecycle, document workflow), game character states, UI components.

Pitfalls: state explosion if you over-divide; states can become coupled if they reference each other to set transitions.

Strategy

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. Reference: https://refactoring.guru/design-patterns/strategy

Problem: Multiple algorithms solve the same problem; client wants to pick at runtime. Hardcoded if algo == "X": blocks make adding algorithms invasive.

from abc import ABC, abstractmethod

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data: list) -> list: ...

class QuickSort(SortStrategy):
    def sort(self, data): return sorted(data)        # imagine real quicksort

class BubbleSort(SortStrategy):
    def sort(self, data):
        # actual bubble sort
        data = data.copy()
        for i in range(len(data)):
            for j in range(len(data) - i - 1):
                if data[j] > data[j + 1]:
                    data[j], data[j + 1] = data[j + 1], data[j]
        return data

class Context:
    def __init__(self, strategy: SortStrategy):
        self.strategy = strategy
    def execute(self, data):
        return self.strategy.sort(data)

ctx = Context(QuickSort())
print(ctx.execute([3, 1, 2]))
ctx.strategy = BubbleSort()              # swap at runtime
print(ctx.execute([3, 1, 2]))

Pythonic shortcut — strategies are often just functions:

def sort_with(data, strategy):
    return strategy(data)

sort_with([3, 1, 2], sorted)
sort_with([3, 1, 2], lambda d: sorted(d, reverse=True))

When to use: many algorithms for one task; algorithm choice should be runtime-configurable; want to avoid conditionals on algorithm type.

Real-world: sorting/comparison functions, payment methods, compression algorithms, authentication backends, retry strategies.

Pitfalls: over-engineering — a function or callable often suffices; don’t create class hierarchies for two-line algorithms.

Template Method

Intent: Define the skeleton of an algorithm in a method, deferring some steps to subclasses. Subclasses redefine certain steps without changing the algorithm’s structure. Reference: https://refactoring.guru/design-patterns/template-method

Problem: Multiple subclasses share most of an algorithm with small variations. Duplicating the skeleton invites drift; pure inheritance for variation is rigid.

from abc import ABC, abstractmethod

class DataPipeline(ABC):
    def process(self):                               # template method (final-ish)
        data = self.extract()
        transformed = self.transform(data)
        self.load(transformed)
        self.notify()

    @abstractmethod
    def extract(self): ...
    @abstractmethod
    def transform(self, data): ...
    @abstractmethod
    def load(self, data): ...
    def notify(self):                                 # hook with default
        print("Pipeline complete")

class CSVPipeline(DataPipeline):
    def extract(self):
        return ["row1,a,b", "row2,c,d"]
    def transform(self, data):
        return [row.split(",") for row in data]
    def load(self, data):
        print(f"Loaded {len(data)} rows")

class JSONPipeline(DataPipeline):
    def extract(self):
        return '[{"x": 1}, {"x": 2}]'
    def transform(self, data):
        import json
        return json.loads(data)
    def load(self, data):
        print(f"Loaded {len(data)} JSON objects")

CSVPipeline().process()
JSONPipeline().process()

Template Method vs Strategy:

  • Template Method: inheritance — subclasses fill in steps of an algorithm.
  • Strategy: composition — strategy objects are swappable.

When to use: algorithm structure is fixed, specific steps vary. Classic in frameworks (Django views, pytest fixtures, ETL pipelines).

Real-world: Django generic views, pytest hooks, frameworks where you override specific methods.

Pitfalls: deep inheritance hierarchies; “fragile base class” — changing the template breaks subclasses.

Visitor

Intent: Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements. Reference: https://refactoring.guru/design-patterns/visitor

Problem: You have a stable set of element classes (tree of AST nodes, document structure). Many different operations want to traverse them (render to HTML, render to PDF, type check, evaluate). Adding each operation as a method to every class is invasive.

from abc import ABC, abstractmethod

# Visitor interface
class Visitor(ABC):
    @abstractmethod
    def visit_number(self, number): ...
    @abstractmethod
    def visit_add(self, add): ...

# Elements accept a visitor
class Expression(ABC):
    @abstractmethod
    def accept(self, visitor): ...

class Number(Expression):
    def __init__(self, value): self.value = value
    def accept(self, visitor): return visitor.visit_number(self)

class Add(Expression):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    def accept(self, visitor): return visitor.visit_add(self)

# Concrete visitors
class Evaluator(Visitor):
    def visit_number(self, n): return n.value
    def visit_add(self, a): return a.left.accept(self) + a.right.accept(self)

class Printer(Visitor):
    def visit_number(self, n): return str(n.value)
    def visit_add(self, a): return f"({a.left.accept(self)} + {a.right.accept(self)})"

expr = Add(Number(3), Add(Number(4), Number(5)))
print(expr.accept(Evaluator()))      # 12
print(expr.accept(Printer()))         # "(3 + (4 + 5))"

Double dispatch: the visitor pattern uses two dispatches — first on the element (element.accept(visitor)), then on the visitor (visitor.visit_X(element)). Selects behavior based on both types.

When to use: stable element hierarchy, many distinct operations over it. ASTs, document trees, syntax checkers, code generators.

Pitfalls:

  • Adding a new element type requires updating ALL visitors. (Inverse of adding a new operation, which is cheap.)
  • Verbose in Python (where simpler patterns suffice). Often replaced by functools.singledispatch or pattern matching (match/case).

Pythonic alternative:

from functools import singledispatch

@singledispatch
def evaluate(node): raise NotImplementedError

@evaluate.register
def _(node: Number): return node.value
@evaluate.register
def _(node: Add): return evaluate(node.left) + evaluate(node.right)

Choosing among behavioral patterns

Situation Pattern
Pass request along a chain of handlers Chain of Responsibility
Encapsulate operations (for undo, queue, logging) Command
Interpret a small custom language Interpreter
Traverse a collection Iterator
Mediate complex object interactions Mediator
Save/restore state without breaking encapsulation Memento
Notify dependents when state changes Observer
Behavior changes based on internal state State
Interchangeable algorithms at runtime Strategy
Algorithm skeleton with overridable steps Template Method
Multiple operations over a stable element hierarchy Visitor

Pattern relationships

  • Strategy vs State: same structure (object holds a strategy/state); intent differs. Strategy is “pick the algorithm”; State is “the object changes behavior as state transitions.”
  • Strategy vs Template Method: composition (Strategy) vs inheritance (Template Method) for varying parts of an algorithm.
  • Observer vs Mediator: Observer is one-to-many notification; Mediator is many-to-many routing.
  • Command vs Strategy: Command is “what to do” as an object (with execute()); Strategy is “how to do it” as a swappable algorithm.
  • Iterator + Composite: Iterator naturally traverses Composite trees.
  • Visitor + Composite: Visitor often operates on Composite hierarchies (especially ASTs).

Common interview confusions

  • “Strategy and State are the same.” — same structure (object holds a behavior object); different intent. Strategy is the client picking an algorithm; State is the object self-transitioning between behaviors.
  • “Observer and Mediator are the same.” — Observer: subject → observers (one-to-many). Mediator: ↔ many objects with central routing. Different shapes.
  • “Iterator is just a for loop.”for is consumer syntax; Iterator is the producer protocol (__iter__, __next__). Custom collections implement Iterator to be for-loopable.
  • “Visitor is the only way to traverse trees.” — visitor pattern is one way; singledispatch, pattern matching, or just polymorphic methods often suffice in Python.

Interview angle

  • “What are the GoF behavioral patterns?” — eleven patterns for communication and behavior: Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor.
  • “Strategy vs State?” — same structure (composed behavior object), different intent. Strategy: client/external code picks an algorithm; algorithms are usually independent. State: the object transitions between states based on internal events; states often reference each other to drive transitions.
  • “Observer vs Mediator?” — Observer broadcasts subject changes to many observers (one-to-many). Mediator centralizes communication among many objects (many-to-many with a central hub). Both reduce coupling but different shapes.
  • “What’s the Command pattern used for?” — encapsulating an action as an object so it can be queued, logged, scheduled, or undone. Classic in GUI buttons, transaction systems, undo/redo, macro recording.
  • “How do you implement Iterator in Python?” — define __iter__ (returning self or a new iterator) and __next__ (returning next value or raising StopIteration). Or just use a generator function — generators are iterators.
  • “When would you use Visitor?” — operations over a stable element hierarchy that you can’t or shouldn’t modify (AST, document tree). When new operations are added often but element types rarely change. In Python, functools.singledispatch or pattern matching often replaces it.
  • “Template Method vs Strategy?” — Template Method uses inheritance (subclass overrides specific steps of a fixed algorithm). Strategy uses composition (different strategy objects swap entirely). Same problem (varying parts of an algorithm), different mechanism.
  • “What’s Chain of Responsibility used for in real-world Python?” — middleware stacks. Django middleware, Flask before/after request handlers, FastAPI middleware: each handler decides to handle, modify, or pass the request.
  • “Why is Interpreter less common today?” — modern projects use parser generators (PLY, lark) or built-in libraries for grammars. Hand-rolled interpreter classes don’t scale beyond toy DSLs.