Behavioral Patterns: Organize Collaboration
Compare all eleven GoF behavioral patterns by the change they isolate, the coupling they reduce, and the runtime costs they add to a system in real code.
Behavioral patterns organize how objects route requests, select policies, move through states, and communicate. This guide compares all eleven GoF patterns through their forces, costs, code sketches, and production failure scenarios, including retries, observer lifecycles, and snapshot retention. Use the comparison map and selection questions to identify the change each pattern isolates, weigh its operational costs, and choose a simpler alternative when direct calls or conditionals explain the design better.
Behavioral Patterns: Organize Collaboration
Introduction
Behavioral design patterns describe how objects hand work to one another. They are most useful when collaboration itself keeps changing: a request may take different routes, a policy may be swapped, or many components may need to react to one event. They are less useful when they only rename a direct method call.
The eleven Gang of Four (GoF) behavioral patterns solve different versions of that problem. This guide compares their forces and costs, sketches the mechanics, and gives a practical way to choose the smallest design that fits. It assumes you know basic interfaces, composition, and polymorphism; the composition-over-inheritance guide is a useful refresher.
A Map of the Patterns
Start with the change you expect. The pattern names are labels for recurring structures, not a checklist every codebase must implement.
| Pattern | Main move | Example pressure | Cost to watch |
|---|---|---|---|
| Chain of Responsibility | Pass a request along handlers | Validation and authorization stages vary | Unhandled requests or surprising order |
| Command | Turn an operation into data | Queue, retry, audit, or undo an action | More types and serialization concerns |
| Interpreter | Evaluate a small language | Rules or filters have a defined grammar | Grammar and evaluator growth |
| Iterator | Traverse without exposing storage | Collections need a stable traversal API | Cursor state and concurrent mutation |
| Mediator | Route peer interactions through a coordinator | Components have too many direct links | A coordinator can become a god object |
| Memento | Capture and restore private state | Undo, checkpoints, or drafts | Memory, versioning, and sensitive snapshots |
| Observer | Notify subscribers after a change | Several consumers react to an event | Lifecycle leaks and delivery ambiguity |
| State | Delegate behavior to the current state | Legal actions vary by lifecycle phase | State-class proliferation |
| Strategy | Swap an algorithm behind a contract | Policies vary independently of a workflow | Indirection for a stable algorithm |
| Template Method | Fix an algorithm skeleton with override steps | Variants share a stable sequence | Inheritance coupling |
| Visitor | Add operations across a stable type set | Many operations traverse a fixed object model | New element types require visitor changes |
flowchart LR
Change[What varies?] --> Route[Request route or recipient]
Change --> Policy[Algorithm or current state]
Change --> Sequence[Workflow skeleton or traversal operation]
Change --> History[Past state or deferred action]
Route --> Chain[Chain of Responsibility]
Route --> Mediator[Mediator]
Route --> Observer[Observer]
Policy --> Strategy[Strategy]
Policy --> State[State]
Sequence --> Template[Template Method]
Sequence --> Visitor[Visitor]
History --> Command[Command]
History --> Memento[Memento]
History --> Iterator[Iterator]
History --> Interpreter[Interpreter]
This is a starting map, not a strict taxonomy. For example, Command often enables undo, but Memento may be needed to preserve the receiver’s private state safely.
The Eleven Patterns in Practice
Chain of Responsibility
Each handler either acts on a request or forwards it. The caller need not know which handler will accept it. Middleware, validation pipelines, and escalation rules are common fits.
for handler in handlers:
result = handler.tryHandle(request)
if result.handled: return result
return notHandled
Keep ordering explicit and decide what happens when no handler accepts the request. A chain that silently falls through can make authorization failures hard to diagnose.
Command
A command packages an operation and its arguments behind an execute() operation. A queue can persist it, a log can audit it, and a UI can retain it for undo. For undo, pair the command with a compensating operation or a memento.
interface Command { void execute(); }
record PublishDraft(Post post) implements Command {
public void execute() { post.publish(); }
}
Do not assume that putting a command in a queue makes it safe to retry. Commands need idempotency or deduplication if delivery can repeat.
Interpreter
Interpreter represents grammar rules as objects and evaluates an expression tree. A permission rule such as role == "editor" AND owns(resource) can be parsed into nodes that implement evaluate(context).
This fits small, constrained languages. For a broad grammar, use a parser library or a dedicated language tool; a home-grown class per grammar production gets difficult to maintain.
Iterator
An iterator exposes traversal operations such as hasNext() and next() while hiding collection storage. A cursor can walk a tree in order without exposing its node links.
Prefer a language’s built-in iteration protocol for ordinary collections. Custom iterators earn their keep when the traversal is domain-specific, lazy, or needs independent cursor state.
Mediator
Colleagues send coordination requests to a mediator instead of calling one another directly. A dialog controller can enable a submit button only when the form’s fields are valid, without every field knowing about every other field.
The mediator reduces pairwise coupling, but it can accumulate all business rules. Split it by workflow or aggregate boundary before it becomes the place where every object delegates its judgment.
Memento
Memento captures an object’s state so it can be restored later without exposing its internal representation. An editor can save a private snapshot before applying an edit; undo restores that snapshot.
Keep snapshots scoped and bounded. Large or sensitive state makes history expensive, and durable snapshots need a versioning strategy when the model changes.
Observer
An observer registers for notifications from a subject. A UI view can refresh after a model changes, or in-process subscribers can react to a domain event.
subject.update(value):
state = value
for listener in listeners: listener.changed(state)
This simple synchronous form has clear ordering and failure behavior. An asynchronous event bus adds delivery, retry, and ordering concerns; it should be treated as messaging infrastructure, not just a renamed observer list. See pub/sub patterns for that distinction.
State
State moves behavior that depends on an object’s current phase into state objects. An order can delegate cancel() to DraftState, PaidState, or ShippedState; each state controls which transitions are legal.
order.cancel(): currentState.cancel(order)
DraftState.cancel(order): order.markCancelled()
ShippedState.cancel(order): reject("already shipped")
This is useful when conditionals recur across several operations. If there are only two stable cases, an enum and a small conditional may be clearer.
Strategy
Strategy places interchangeable algorithms behind one interface. The caller selects the policy; the strategy performs the calculation.
interface TaxPolicy { Money calculate(Invoice invoice); }
final class Checkout {
private final TaxPolicy taxPolicy;
Checkout(TaxPolicy taxPolicy) { this.taxPolicy = taxPolicy; }
Money total(Invoice invoice) { return invoice.subtotal().plus(taxPolicy.calculate(invoice)); }
}
Tax rules can vary by jurisdiction without branching through checkout. This is a focused use of polymorphism in Java: put the variation behind a contract where it belongs.
Template Method
Template Method defines the order of an algorithm in a base class and lets subclasses override selected steps. A report exporter might fix load → transform → write, while subclasses choose the file format.
It works when the sequence is stable and extensions are well understood. If variants need runtime selection or multiple independent behaviors, composition with Strategy usually avoids a subclass tree.
Visitor
Visitor adds operations over a set of element types without placing every operation on those element classes. A compiler may use visitors for printing, type checking, and code generation over the same syntax tree.
The trade-off is a closed set of element types: adding a new node type means updating visitors. Choose it when the object model changes rarely and operations change more often. If types are added frequently, ordinary polymorphic methods are usually less disruptive.
Choosing Among Similar Patterns
Several patterns can look alike from a distance. Ask what varies and who should own the choice:
- Strategy vs. State: Strategy is usually selected by a caller or configuration; State changes as the object’s lifecycle advances and often controls transitions.
- Observer vs. Mediator: Observer broadcasts that something changed; Mediator coordinates a specific interaction among colleagues.
- Command vs. Strategy: Command represents a request that may be queued, logged, or undone; Strategy supplies an algorithm used during a call.
- Template Method vs. Strategy: Template Method fixes a sequence and uses inheritance for variable steps; Strategy delegates a replaceable algorithm through composition.
- Chain vs. Mediator: Chain forwards until a handler accepts; Mediator routes interactions among known participants.
- Memento vs. Command: Memento stores state; Command stores an operation. Undo often combines them.
The separation of concerns guide discusses a nearby design question: whether behavior belongs with the object that owns the data.
When to Use
Reach for a behavioral pattern when a concrete change pressure is already visible:
- Add a new policy without rewriting a stable workflow: Strategy.
- Add legal lifecycle transitions without scattering phase checks: State.
- Let independent consumers react to a change: Observer.
- Coordinate peers whose direct dependencies are multiplying: Mediator.
- Carry a request across a queue, audit boundary, or undo history: Command.
- Add handlers without changing the caller: Chain of Responsibility.
- Hide collection storage or define a custom traversal: Iterator.
- Preserve private state for undo or checkpoints: Memento.
- Add operations to a relatively stable object structure: Visitor.
- Extend a fixed algorithm skeleton: Template Method.
- Evaluate a small domain-specific grammar: Interpreter.
The trigger is not “this code has a conditional.” It is that the same kind of change keeps touching the same stable collaborators, or a caller must know too much about a collaboration.
When NOT to Use
Skip the pattern when the design is easier to explain as a direct call, a small function, a map of handlers, or a straightforward conditional. One algorithm with no planned variation rarely needs a Strategy interface. One observer rarely needs a registration framework.
Avoid pattern-first design when:
- The variation has not appeared and is not required by the domain.
- A pattern introduces more classes than the behavior it isolates.
- You cannot state the ownership rule, ordering rule, and failure behavior.
- An existing language feature already provides the same abstraction.
- The abstraction makes debugging or tracing harder than the original code.
Patterns are vocabulary for communicating a design. They are not a scorecard for object-oriented code.
Production Failure Scenarios
A handler chain drops a request
A new request subtype reaches the end of a chain without a matching handler. If the default behavior is “success with no action,” users see a partial operation while logs show no obvious exception. Make the terminal behavior explicit, measure unhandled requests, and test representative paths through the chain.
An observer outlives its subject
A long-lived publisher retains a short-lived listener. The listener remains reachable and receives callbacks after its UI or request scope has ended. Tie registration to a lifecycle and make unsubscription automatic where possible. In asynchronous systems, also define replay, ordering, and duplicate delivery.
A command is retried twice
A worker times out after a side effect but before acknowledging the command. The queue redelivers it, and a non-idempotent payment or email operation happens twice. Include a stable command ID, persist deduplication state at the effect boundary, and separate retryable failures from permanent ones.
A mediator becomes a hidden transaction coordinator
One coordinator updates inventory, payment, and shipping state in sequence. A process crash between steps leaves a partial result. Keep local object coordination distinct from distributed transaction or workflow design; use explicit persistence, idempotency, and compensation where the operation crosses services.
A memento captures too much
Full snapshots of large documents fill memory or accidentally retain secrets after a user logs out. Bound history, redact sensitive fields, and document whether snapshots are in-memory only or durable. When state is persisted, include schema version and migration behavior.
Trade-Off Table
| Pattern | Buys you | Pays for | Simplest alternative |
|---|---|---|---|
| Chain of Responsibility | Extensible routing | Ordering and terminal-case complexity | Ordered function list |
| Command | Deferred, logged, or undoable requests | Request types, IDs, retry semantics | Direct function call |
| Interpreter | A model for a small grammar | Parser and evaluator maintenance | Parse a few explicit conditions |
| Iterator | Encapsulated, custom traversal | Cursor lifetime and mutation rules | Built-in collection iteration |
| Mediator | Fewer peer-to-peer dependencies | Central coordination pressure | Direct calls among a small group |
| Memento | Encapsulated restore points | Storage, retention, and versioning | Recompute or store selected fields |
| Observer | Multiple reactions without publisher knowing consumers | Lifecycle, ordering, delivery, and failure policy | Direct call to one consumer |
| State | Localized lifecycle behavior | More state types and transition logic | Enum plus a small switch |
| Strategy | Replaceable policies | Interface and selection plumbing | Function parameter or conditional |
| Template Method | Shared algorithm order | Inheritance constraints | One function with callbacks |
| Visitor | New operations over stable types | Visitor updates when types change | Methods on elements or pattern matching |
Observability Checklist
- Record the request or workflow identifier as behavior crosses handlers, commands, or mediators.
- Track chain length, selected handler, and requests that reach the terminal case.
- For commands, record enqueue, attempt, outcome, retry count, deduplication result, and completion latency.
- For observers, monitor active subscriptions, callback failures, queue lag, and event age.
- For stateful objects, log valid transitions with prior and next state; count rejected transitions without logging secrets.
- For undo or memento flows, measure snapshot size, history depth, restore failures, and retention cleanup.
- Tag the chosen strategy or interpreter rule version when results must be reproduced.
- Keep logs structured and bounded; do not dump command payloads or snapshots by default.
Security and Compliance Notes
Behavioral patterns can move sensitive data across boundaries. Commands placed on durable queues need access controls, encryption, retention limits, and payload minimization. A command log is an audit surface; restrict who can read it and avoid copying credentials or payment data into it.
Observer callbacks may run with the publisher’s authority or under a worker identity. Check authorization at the point where the side effect occurs; subscribing to an event should not grant permission to perform every resulting action. Validate event and command payloads at trust boundaries, and do not trust a serialized class name to choose executable behavior.
Mementos and undo history may contain data a user later deletes. Define retention and deletion semantics for snapshots, caches, and backups. For regulated workflows, record who initiated a transition, which policy version applied, and the outcome, while keeping the audit record separate from untrusted user-controlled fields.
Common Pitfalls and Anti-Patterns
- Pattern names without a change reason: a class called
Strategydoes not help if there is only one permanently fixed algorithm. - God mediator: all domain decisions pass through one coordinator. Split by use case or let cohesive objects own their rules.
- Observer as hidden control flow: a synchronous callback throws and prevents later listeners from running. Decide whether one listener failure should stop delivery.
- Unbounded command history: queued commands or undo records accumulate forever. Set retention and back-pressure policies.
- State objects that only wrap conditionals: if they do not own meaningful state-specific behavior or transitions, a simple enum may read better.
- Subclass hooks with unclear invariants: Template Method extensions can violate required ordering. Make hook contracts explicit and test the sequence.
- Visitor for an open type system: every new element forces edits to all visitors. Choose a dispatch approach that fits how often the element set changes.
- Interpreter as a general-purpose language: ad hoc grammars grow without precedence, limits, or error reporting. Define the grammar before users depend on it.
- Confusing notification with durable messaging: an in-process Observer does not provide persistence, delivery guarantees, or replay.
Quick Recap Checklist
- Name the kind of change or collaboration pressure you are isolating.
- Identify who selects behavior and who owns each state transition.
- State the ordering, failure, retry, and lifecycle rules.
- Compare the pattern with a function, conditional, or built-in language feature.
- Check whether the object types or the operations are more likely to change.
- Add observability and retention rules if requests or state become durable.
- Keep the pattern only if the resulting code is easier to change and explain.
Interview Questions
Strategy usually represents a policy chosen by a caller or configuration, such as a tax calculation. State represents a lifecycle phase owned by an object, such as an order that permits cancellation while it is a draft but rejects it after shipment. State often controls transitions; Strategy usually does not.
Choose Command when the request needs an identity or a lifetime beyond the current call: it may be queued, retried, audited, scheduled, or undone. A direct call is simpler when it runs immediately and needs no separate history or transport. Retried commands also need idempotency or deduplication.
Observers hide control flow behind notifications. A listener can leak if it is never removed, and a callback failure can affect delivery to later listeners. Asynchronous observers add questions about ordering, retries, duplicates, and durable storage, so those guarantees must be explicit.
Further Reading
- Refactoring.Guru: Behavioral Design Patterns — visual overviews and intent for the GoF behavioral patterns.
- Design Patterns: Elements of Reusable Object-Oriented Software — the original GoF catalog and discussion of pattern trade-offs.
- Separation of concerns and module boundaries — a broader view of assigning responsibilities and keeping dependencies local.
Conclusion
Behavioral patterns organize who handles a request, how a policy changes, and how collaborators communicate. Chain, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, and Visitor each isolate a different kind of variation. Pick based on the change you expect, then account for the costs the pattern adds: more indirection, lifecycle rules, state, storage, or failure paths. When a conditional or direct call explains the behavior more clearly, keep it.
Category
Related Posts
Choosing, Combining, and Removing Patterns
Choose GoF patterns by tracing change pressure, comparing direct code with pattern roles, and removing abstractions when maintenance costs exceed their value.
Class Diagrams for Design Conversations
Use a compact UML class diagram to discuss ownership, multiplicity, and dependencies in object-oriented designs before those choices harden into code.
Creational Patterns: Control Object Creation
Compare five creational patterns, see a compact TypeScript example, and choose the simplest fit for product families, construction, copying, or shared state.