The third group of GoF patterns is about behavior: which object does what, and how objects hand work to each other. Of the eleven behavioral patterns in the book, the six on this page are the ones you meet every day, often without noticing, because modern languages and frameworks have built them in. Many of them become simpler with higher-order functions, functions that take or return other functions: where the 1994 book needs an interface and a class, Python often needs one function. The examples follow Kestrel Energy's billing system, KBS, and its meter-api.

Strategy: swappable algorithms

KBS calculates a day's cost differently for each contract type: one price for a fixed contract, a day and a night price for a day-night contract, the hourly market price for a dynamic contract. The strategy pattern puts each variant of an algorithm behind the same interface, so the code that uses it can pick one at run time and the variants can change independently of that code. In Python, the shared interface can simply be a function signature:

from decimal import Decimal

# Hourly market prices for one day: cheap at night, expensive at the evening peak.
MARKET = [Decimal("0.18")] * 7 + [Decimal("0.27")] * 10 + [Decimal("0.41")] * 4 + [Decimal("0.24")] * 3


# Three strategies with the same signature: 24 hourly kWh values -> cost.
def fixed(hourly_kwh):
    return sum(hourly_kwh) * Decimal("0.2700")


def day_night(hourly_kwh):
    night = sum(kwh for hour, kwh in enumerate(hourly_kwh) if hour < 7 or hour >= 23)
    return night * Decimal("0.2240") + (sum(hourly_kwh) - night) * Decimal("0.3120")


def dynamic(hourly_kwh):
    return sum(kwh * price for kwh, price in zip(hourly_kwh, MARKET))


STRATEGIES = {"fixed": fixed, "day-night": day_night, "dynamic": dynamic}


def daily_cost(contract_type, hourly_kwh):
    calculate = STRATEGIES[contract_type]      # chosen at run time
    return round(calculate(hourly_kwh), 2)


# One household's day, with an evening peak from 17:00 to 21:00.
usage = [Decimal("0.3")] * 7 + [Decimal("0.5")] * 10 + [Decimal("1.2")] * 4 + [Decimal("0.6")] * 3
print(f"usage: {sum(usage)} kWh")
for contract_type in STRATEGIES:
    print(f"{contract_type:10} EUR {daily_cost(contract_type, usage)}")
usage: 13.7 kWh
fixed      EUR 3.70
day-night  EUR 4.04
dynamic    EUR 4.13

daily_cost contains no if per contract type, and a new contract type is one new function plus one dictionary entry. You use strategies all the time: the key argument of sorted() and the Comparator passed to a Java sort are strategies for "how to compare". In Java and C#, a strategy is an object that implements an interface, often written as a lambda for a functional interface. The factory on the previous page and a strategy often work together: the factory picks the strategy from the contract record.

Observer: notifying whoever is interested

When meter-api stores a new reading, several other things should happen: the cached usage graph must be refreshed, and a customer with very high usage gets a warning by email. Next year there will be a third reaction. If the store calls each of them by name, every new reaction means changing the store.

The observer pattern lets an object (the subject) keep a list of interested objects or functions (the observers) and notify all of them when something changes, without knowing what they do. Observers register and unregister themselves:

class ReadingStore:
    """Subject: keeps a list of observers and notifies them of new readings."""

    def __init__(self):
        self._observers = []

    def subscribe(self, observer):
        self._observers.append(observer)

    def unsubscribe(self, observer):
        self._observers.remove(observer)

    def save(self, ean, kwh):
        print(f"stored {ean}: {kwh} kWh")
        for notify in self._observers:
            notify(ean, kwh)


# Observers: plain functions with the same signature.
def refresh_usage_graph(ean, kwh):
    print(f"  graph cache for {ean} cleared")


def check_high_usage(ean, kwh):
    if kwh > 2.0:
        print(f"  high-usage email queued for {ean}")


store = ReadingStore()
store.subscribe(refresh_usage_graph)
store.subscribe(check_high_usage)

store.save("E-0412", 0.4)
store.save("E-0413", 3.1)

store.unsubscribe(refresh_usage_graph)
store.save("E-0412", 0.5)
stored E-0412: 0.4 kWh
  graph cache for E-0412 cleared
stored E-0413: 3.1 kWh
  graph cache for E-0413 cleared
  high-usage email queued for E-0413
stored E-0412: 0.5 kWh

Each observer here is a callback function. The same pattern is everywhere in user interfaces: addEventListener in the browser registers an event listener, which is an observer of a button or a page.

In TypeScript, generics can make the observer type-safe: each event name is tied to the shape of its payload, so a listener for readingStored knows it receives a reading, and on() returns a function that unsubscribes again:

interface Reading {
  ean: string;
  kwh: number;
}

type Listener<T> = (event: T) => void;

// A small, typed subject: listeners for each event name get the right payload.
class Events<Map extends Record<string, unknown>> {
  private listeners: { [K in keyof Map]?: Listener<Map[K]>[] } = {};

  on<K extends keyof Map>(name: K, listener: Listener<Map[K]>): () => void {
    const list = (this.listeners[name] ??= []);
    list.push(listener);
    return () => {
      const i = list.indexOf(listener);
      if (i >= 0) list.splice(i, 1); // unsubscribe (safe to call twice)
    };
  }

  emit<K extends keyof Map>(name: K, event: Map[K]): void {
    for (const listener of this.listeners[name] ?? []) listener(event);
  }
}

const events = new Events<{ readingStored: Reading; meterReplaced: { ean: string } }>();

const stop = events.on("readingStored", (r) => console.log(`graph cache for ${r.ean} cleared`));
events.on("readingStored", (r) => {
  if (r.kwh > 2.0) console.log(`high-usage email queued for ${r.ean}`);
});
events.on("meterReplaced", (e) => console.log(`new meter at ${e.ean}`));

events.emit("readingStored", { ean: "E-0413", kwh: 3.1 });
stop();
events.emit("readingStored", { ean: "E-0412", kwh: 0.5 });
events.emit("meterReplaced", { ean: "E-0412" });
graph cache for E-0413 cleared
high-usage email queued for E-0413
new meter at E-0412

The payoff of the types is that mistakes in what is sent to observers are caught before anything runs. A typo in a payload is a compile error, not a listener that quietly reads undefined:

interface Reading {
  ean: string;
  kwh: number;
}

type Listener<T> = (event: T) => void;

const listeners: Listener<Reading>[] = [];

function emit(reading: Reading): void {
  for (const listener of listeners) listener(reading);
}

listeners.push((r) => console.log(r.ean.toUpperCase()));
emit({ ean: "E-0412", kwH: 0.5 });
example.ts(15,23): error TS2561: Object literal may only specify known properties, but 'kwH' does not exist in type 'Reading'. Did you mean to write 'kwh'?

Command: requests as objects

Lucía Romero, a billing clerk, corrects invoices all day: credit an amount for a wrong meter reading, a goodwill gesture after an outage. Each correction must be logged for audit, and a mistake in the last correction should be undoable without a database administrator.

The command pattern turns a request into an object that holds everything needed to carry it out later. Because it is an object, it can be stored in a queue or a log, run by code that does not know what it does, and given an undo() method:

from dataclasses import dataclass
from decimal import Decimal


class Account:
    def __init__(self, customer_id, balance):
        self.customer_id = customer_id
        self.balance = Decimal(balance)


@dataclass
class CreditInvoice:
    """Command: a request turned into an object, with an undo."""
    account: Account
    amount: Decimal
    reason: str

    def execute(self):
        self.account.balance -= self.amount

    def undo(self):
        self.account.balance += self.amount

    def describe(self):
        return f"credit {self.amount} to {self.account.customer_id} ({self.reason})"


class CorrectionLog:
    """Invoker: runs commands, keeps an audit trail and an undo stack."""

    def __init__(self):
        self.done = []

    def run(self, command, clerk):
        command.execute()
        self.done.append(command)
        print(f"{clerk}: {command.describe()}")

    def undo_last(self, clerk):
        command = self.done.pop()
        command.undo()
        print(f"{clerk}: UNDO {command.describe()}")


account = Account("C-10442", "118.65")
log = CorrectionLog()
log.run(CreditInvoice(account, Decimal("12.40"), "wrong meter reading"), "lucia")
log.run(CreditInvoice(account, Decimal("150.00"), "goodwill"), "lucia")   # meant 15.00
log.undo_last("lucia")
log.run(CreditInvoice(account, Decimal("15.00"), "goodwill"), "lucia")
print("balance:", account.balance)
lucia: credit 12.40 to C-10442 (wrong meter reading)
lucia: credit 150.00 to C-10442 (goodwill)
lucia: UNDO credit 150.00 to C-10442 (goodwill)
lucia: credit 15.00 to C-10442 (goodwill)
balance: 91.25

CorrectionLog runs, records and undoes commands without knowing what a credit is; a CorrectReading or CancelInvoice command plugs into the same log. Undo stacks in editors, job queues that store "work to do" for a worker, and database migrations with an "up" and a "down" step are all commands. When the command only needs to be run, never undone or stored, a function or a lambda is the simpler Python version.

State: behavior that depends on the current state

A contract is pending, active, moving or ended, and the same request means something different in each: "move" on an active contract starts a final meter reading, on a pending contract it is an error. Written with if statements, every method of Contract repeats the same check of the current state, and the rules of one state are spread over all of them.

The state pattern gives each state its own class with the same methods; the object delegates every request to its current state object, which does the work and decides the next state. To the caller, the object seems to change its class when its state changes:

Contract states. Pending goes to Active with activate. Active goes to Moving with request_move, and Moving goes back to Active with activate. Active goes to Ended with end. Pending Active Ended Moving activate() end() request_move() activate()
The contract's states and the requests that move it from one to the next. Every request that has no arrow from the current state is refused.
class Contract:
    """Context: delegates to its current state object."""

    def __init__(self, number):
        self.number = number
        self.state = Pending()

    def activate(self):
        self.state = self.state.activate(self)

    def request_move(self):
        self.state = self.state.request_move(self)

    def end(self):
        self.state = self.state.end(self)


class State:
    """Default: the action is not allowed in this state."""

    def refuse(self, contract, action):
        print(f"{contract.number}: cannot {action} while {self.name}")
        return self

    def activate(self, contract):
        return self.refuse(contract, "activate")

    def request_move(self, contract):
        return self.refuse(contract, "move")

    def end(self, contract):
        return self.refuse(contract, "end")


class Pending(State):
    name = "pending"

    def activate(self, contract):
        print(f"{contract.number}: supply starts, first advance payment scheduled")
        return Active()


class Active(State):
    name = "active"

    def request_move(self, contract):
        print(f"{contract.number}: final meter reading requested for the old address")
        return Moving()

    def end(self, contract):
        print(f"{contract.number}: final invoice scheduled")
        return Ended()


class Moving(State):
    name = "moving"

    def activate(self, contract):
        print(f"{contract.number}: supply continues at the new address")
        return Active()


class Ended(State):
    name = "ended"


contract = Contract("CT-20417")
contract.request_move()          # not allowed yet
contract.activate()
contract.request_move()
contract.end()                   # not allowed during a move
contract.activate()
contract.end()
print("final state:", contract.state.name)
CT-20417: cannot move while pending
CT-20417: supply starts, first advance payment scheduled
CT-20417: final meter reading requested for the old address
CT-20417: cannot end while moving
CT-20417: supply continues at the new address
CT-20417: final invoice scheduled
final state: ended

Everything an active contract may do is in Active; a new state such as "suspended" is a new class, and the base class makes "not allowed" the safe default. Network protocols are state machines in the same sense: a TCP connection behaves differently in each of its states.

Modern Java: sealed types and switch

Modern Java offers a different way to model the same thing. A sealed interface lists every possible state, each state is a record that can carry its own data (the due date of a sent invoice, the number of reminders of an overdue one), and a switch with pattern matching holds all the transitions in one function. This invoice lifecycle uses a compact source file:

import java.time.LocalDate;

enum Event { SEND, PAY, DUE_DATE_PASSED }

// Every possible state, and nothing else: the compiler knows the full list.
sealed interface InvoiceState permits Draft, Sent, Overdue, Paid {}
record Draft() implements InvoiceState {}
record Sent(LocalDate due) implements InvoiceState {}
record Overdue(int reminders) implements InvoiceState {}
record Paid() implements InvoiceState {}

InvoiceState next(InvoiceState state, Event event) {
    return switch (state) {
        case Draft _ -> event == Event.SEND ? new Sent(LocalDate.of(2026, 11, 7)) : refuse(state, event);
        case Sent _ -> switch (event) {
            case PAY -> new Paid();
            case DUE_DATE_PASSED -> new Overdue(1);
            default -> refuse(state, event);
        };
        case Overdue o -> switch (event) {
            case PAY -> new Paid();
            case DUE_DATE_PASSED -> new Overdue(o.reminders() + 1);
            default -> refuse(state, event);
        };
        case Paid _ -> refuse(state, event);
    };
}

InvoiceState refuse(InvoiceState state, Event event) {
    IO.println("  refused: " + event + " in " + state);
    return state;
}

void main() {
    InvoiceState state = new Draft();
    Event[] events = {Event.PAY, Event.SEND, Event.DUE_DATE_PASSED,
                      Event.DUE_DATE_PASSED, Event.PAY, Event.SEND};
    for (Event event : events) {
        state = next(state, event);
        IO.println(event + " -> " + state);
    }
}
  refused: PAY in Draft[]
PAY -> Draft[]
SEND -> Sent[due=2026-11-07]
DUE_DATE_PASSED -> Overdue[reminders=1]
DUE_DATE_PASSED -> Overdue[reminders=2]
PAY -> Paid[]
  refused: SEND in Paid[]
SEND -> Paid[]

Because the interface is sealed, the compiler knows the complete list of states. Add a record Canceled() state to the permits list and forget the switch, and the program no longer compiles: error: the switch expression does not cover all possible input values. The two styles divide the work differently. With a class per state, adding a state touches one place; with the switch, adding an operation touches one place and the compiler finds every switch that misses the new state. The Java page on modeling variants shows more of this style.

Template method: a fixed skeleton

Joost Bakker's nightly export at 02:30 always follows the same steps: select the invoices, format them, archive the file. Only some steps differ per receiver. A template method is a method in a base class that defines the steps of an algorithm and their order, and calls methods that subclasses fill in or may override (often called hooks):

from abc import ABC, abstractmethod
from decimal import Decimal


class NightlyExport(ABC):
    """Template method: run() fixes the steps and their order."""

    def run(self, invoices):
        rows = [i for i in invoices if self.include(i)]
        print(f"[{self.name}] {len(rows)} of {len(invoices)} invoices selected")
        for line in self.format(rows):
            print(f"[{self.name}] {line}")
        print(f"[{self.name}] archived as {self.file_name()}")

    # Hook with a default: subclasses MAY override it.
    def include(self, invoice):
        return True

    # Steps that subclasses MUST provide.
    @abstractmethod
    def format(self, rows): ...

    @abstractmethod
    def file_name(self): ...


class BankExport(NightlyExport):
    name = "bank"

    def include(self, invoice):          # only direct debits go to the bank
        return invoice["method"] == "direct debit"

    def format(self, rows):
        return [f"{r['invoice']};{r['amount']}" for r in rows]

    def file_name(self):
        return "kestrel-collect-2026-10-07.csv"


class LedgerExport(NightlyExport):
    name = "ledger"

    def format(self, rows):
        total = sum(Decimal(r["amount"]) for r in rows)
        return [f"TOTAL {len(rows)} invoices {total}"]

    def file_name(self):
        return "KESTREL_20261007.xml"


invoices = [
    {"invoice": "INV-2026-104412", "amount": "118.65", "method": "direct debit"},
    {"invoice": "INV-2026-104413", "amount": "96.10", "method": "bank transfer"},
    {"invoice": "INV-2026-104414", "amount": "71.30", "method": "direct debit"},
]
for export in (BankExport(), LedgerExport()):
    export.run(invoices)
[bank] 2 of 3 invoices selected
[bank] INV-2026-104412;118.65
[bank] INV-2026-104414;71.30
[bank] archived as kestrel-collect-2026-10-07.csv
[ledger] 3 of 3 invoices selected
[ledger] TOTAL 3 invoices 286.05
[ledger] archived as KESTREL_20261007.xml

run() is the template method: no subclass can forget to archive or change the order of the steps. include() is a hook with a sensible default, and format() and file_name() are abstract, so a subclass that leaves them out cannot even be created.

Template method is how most frameworks work. Python's unittest calls your setUp(), then a test method, then tearDown(); a web framework calls your request handler; a game engine calls your update() sixty times a second. In each case the framework owns the flow and calls your code at fixed points. This reversal is known as the Hollywood principle: "don't call us, we'll call you". Your code does not drive the process; it is called by the code that does, which is also what separates a framework (it calls you) from a library (you call it).

Iterator: walking through a collection

An iterator is an object that gives access to the elements of a collection one at a time, without revealing how the collection is stored: a list, a tree, a database table read page by page or a file read line by line all look the same to the loop that uses them. The pattern is so useful that languages have built it in. Java has the Iterator interface behind its for-each loop, and Python has the iteration protocol (__iter__ and __next__) behind every for loop.

In Python you rarely write an iterator class: a generator function is an iterator. This one reads quarter-hour readings from the database in pages of three, and the caller simply loops:

def readings(ean, page_size=3):
    """Iterator as a generator: fetches page by page, yields one reading at a time."""
    # Stands in for db01: 7 quarter-hour readings for one meter.
    table = [("00:00", 0.21), ("00:15", 0.19), ("00:30", 0.20), ("00:45", 0.24),
             ("01:00", 0.18), ("01:15", 0.17), ("01:30", 0.22)]
    offset = 0
    while True:
        page = table[offset:offset + page_size]
        if not page:
            return
        print(f"  (fetched rows {offset + 1}-{offset + len(page)} for {ean})")
        yield from page
        offset += page_size


# The caller just loops; paging is the iterator's business.
for quarter, kwh in readings("E-0412"):
    if kwh > 0.23:
        print(f"{quarter} {kwh:.2f} kWh: above 0.23, stopping early")
        break
    print(f"{quarter} {kwh:.2f} kWh")
  (fetched rows 1-3 for E-0412)
00:00 0.21 kWh
00:15 0.19 kWh
00:30 0.20 kWh
  (fetched rows 4-6 for E-0412)
00:45 0.24 kWh: above 0.23, stopping early

The output shows the benefit: the loop stopped after the fourth reading, so the third page was never fetched. A generator only does the work for the elements that are actually used, which matters when a meter has 35,040 quarter-hour readings a year.

Where you already use them

Most developers use these patterns long before they learn the names. Recognizing them helps when you read a framework's documentation or someone else's code:

Pattern Everyday example Python shortcut
Strategy sorted(key=...), Java's Comparator, choosing a retry policy pass a function
Observer addEventListener, change notifications in user interface frameworks, signal handlers a list of callables
Command undo in an editor, job queues, database migrations a function, or an object when it must be stored
State TCP connection states, order and ticket workflows a class per state, or a transition table
Template method unittest's setUp and tearDown, framework request handlers a function that takes the varying steps
Iterator every for loop, file objects, database cursors a generator

The other five behavioral patterns of the GoF book are less common in business code. Chain of responsibility passes a request along a chain of handlers until one deals with it, as web middleware does. Mediator lets objects talk through one central object instead of to each other. Memento saves an object's state so it can be restored. Visitor adds operations to a fixed set of classes, and is largely replaced by pattern matching in modern Java, C# and Python. Interpreter represents a small language as a tree of objects.