core/design_patterns/behavioral.md

Behavioral Design Patterns: Documented Obsolescence and Decision Rules

Which GoF behavioral patterns are now language-feature workarounds (Norvig's 16/23 analysis), named per-pattern verdicts for Strategy, Command, Visitor, State, Observer, Mediator, Chain of Responsibility, Template Method, Iterator in modern languages, and their idiomatic 2020s replacements - with sources.

Assume the reader has the GoF catalog memorized. This document is the part that isn't in the textbook: which behavioral patterns are now considered language-feature workarounds rather than genuine designs, named sources for each verdict, and the concrete signal that tells you which side of the line you're on.

The baseline critique: Norvig, 1996, still the reference point

Peter Norvig's "Design Patterns in Dynamic Programming" (Object World, May 1996) is the original, most-cited source for this entire line of critique: 16 of the 23 GoF patterns become invisible or trivially simple in a language with first-class functions, first-class types, macros, multimethods, and modules - because the pattern's ceremony exists specifically to compensate for the host language (C++/Smalltalk-era Java) lacking that feature. For the behavioral catalog specifically, Norvig's mapping is:

PatternEliminated/simplified byMechanism
Command, Strategy, Template Method, VisitorFirst-class functionsPass a function/closure instead of instantiating a single-method interface
Iterator, InterpreterMacros / language-level generatorsThe language already provides lazy sequence traversal
Mediator, ObserverMethod combination (:before/:after/:around in CLOS)The dispatch mechanism itself handles multi-party notification

This is not a fringe opinion - a peer-reviewed 2019 follow-up, Bafatakis et al., "On the Interaction of Object-Oriented Design Patterns and Programming Languages" (arXiv:1905.13674), reaches the same conclusion independently: patterns are "distorted or overly complicated because of the lack of supporting programming language constructs," and names the same fix list - subtyping separate from inheritance, lexically-scoped closures independent of classes, multimethod dispatch. Treat "does my language already have the feature this pattern is emulating" as the first question before reaching for any pattern below.

Per-Pattern Verdict

Pattern2020s verdictReplace withNamed source
StrategyLegacy ceremony for the stateless casePlain function/closure parameterNorvig 1996; Shashtari, "Strategy pattern is better when done with functions"
CommandLegacy ceremony unless undo/queue/audit is a real requirementClosure/lambda; keep the class only for serializable or undoable actionsNystrom, Game Programming Patterns ("Command" chapter); journal.stuffwithstuff.com, "Closures and the Command Pattern"
VisitorActively being replaced in the languages that popularized itSealed types/enums + exhaustive switch/pattern matchingnipafx, "Visitor Pattern Considered Pointless"; JEP 409 (sealed classes) + JEP 441 (pattern matching for switch), Java 21
Template MethodNamed "hate" target; fragile-base-class failure mode is well documentedComposition: pass the varying step in as a function/strategy, not an override pointMiller, "Patterns I Hate #2: Template Method"; standard composition-over-inheritance literature
StateSound pattern; verdict depends on language, not eraSum type/enum + exhaustive match in languages that have them; keep the class form in languages that don'tNorvig 1996 (first-class types); general ADT literature
ObserverSound pattern; the specific documented failure is memory retention, not obsolescenceSame pattern, but with weak references or explicit unsubscribe/dispose, or move to a broker at scaleWikipedia, "Lapsed listener problem"; gameprogrammingpatterns.com/observer.html
MediatorSound pattern; specific documented failure is scope creep into a God ObjectSame pattern, but mediator handles routing only, never business logicslidetodoc.com summary of GoF Mediator; God Object literature (Riel)
Chain of ResponsibilitySound pattern (middleware is this, everywhere); documented cost is debuggabilitySame pattern, but log entry/exit per handler and keep chain assembly visible at the composition root, not dynamically discoveredMultiple production write-ups converge on "hard to trace which handler acted" as the recurring complaint
IteratorFully absorbed into language runtimesNative generator/iterator protocol (Python generators, Rust Iterator, Go iter.Seq, JS iterables)Norvig 1996 (macros); language specs themselves

Strategy and Command: the closure test

Norvig's point, restated as an operational rule: if the "strategy" or "command" object has no fields and one method, it is a function wearing a costume. Write it as a function. Reserve the class form for exactly two cases:

  1. The behavior carries configuration/state beyond its single call (ExponentialBackoffPolicy holding tunable constants - a closure over those constants is equally valid and often shorter).
  2. The object must satisfy a richer contract than "call it" - undo, serialize, replay, inspect without executing (Command's execute/undo pair is a genuine reason a bare closure doesn't suffice; a closure cannot be introspected or serialized to a queue).

Robert Nystrom's Game Programming Patterns - an unusually well-regarded, still-actively-cited (2014, ongoing citations through 2026) reworking of the GoF catalog for a language-agnostic modern audience - makes the same call for Command specifically: use a first-class function when the action is fire-and-forget; keep the object when you need undo, queuing, or replay logging, because those require holding onto the action's data, not just invoking it once.

Visitor: the pattern actively being deprecated by its host languages

This is the most concrete case in the whole catalog of a language closing the gap the pattern was plugging. Java's sealed classes (finalized JDK 17, JEP 409) plus pattern matching for switch (finalized JDK 21, JEP 441) let a compiler exhaustively check every subtype of a closed hierarchy directly in a switch expression - no accept(), no double dispatch, no per-node-type boilerplate. nipafx (Nicolai Parlog, a widely cited Java community voice) titles the piece bluntly: "Visitor Pattern Considered Pointless - Use Pattern Switches Instead." Kotlin's sealed class + when, and Rust's enum + match, have offered the same capability since their respective 1.0 releases; Visitor was never idiomatic in either language to begin with.

Decision rule: if your language has closed/sealed sum types with exhaustive pattern matching, do not implement Visitor - implement a match/switch per operation over the sealed type and let the compiler's exhaustiveness check replace what accept() used to guarantee at runtime. Only reach for actual Visitor-with-accept() in a language lacking both sealed types and exhaustiveness checking (pre-2017 Java, most dynamically typed languages) where the alternative is an untyped isinstance chain.

Template Method: the fragile-base-class case study

Alex Miller's "Patterns I Hate #2: Template Method" (puredanger, 2007 - predates the modern critique wave but is the direct ancestor of it and still the most-cited single source) names the mechanism precisely: the subclass is contractually bound to a base class it usually cannot see the whole of, is restricted to overriding pre-declared hook points, and cannot reorder or add steps - and a change to the base algorithm's skeleton silently changes every subclass's behavior at once. This is the same fragile-base-class failure mode documented for inheritance generally.

Decision rule: when the "varying step" is a single function parameter, pass it as a function (this is Strategy, not Template Method, and needs no inheritance at all). Reserve actual Template Method for cases where multiple steps share significant ordering and state-passing logic between them that would be awkward to thread through a plain function's parameter list.

Observer: sound pattern, one specific, well-named failure mode

Unlike Visitor or Template Method, Observer is not being replaced - event-driven architecture is Observer, ubiquitously. The specific, named, documented failure is the lapsed listener problem (its own Wikipedia entry): a subject holds a strong reference to every registered observer, so an observer that forgets to unsubscribe is retained by the garbage collector for the subject's entire lifetime, even after every other reference to it is gone - the canonical example is a UI screen destroyed but still receiving (and paying CPU for) notifications because it stayed in a long-lived subject's listener list.

Decision rule: any Observer whose subject outlives its typical observer (a long-lived model observed by short-lived views) needs either weak references on the subject's listener list, or an explicit, enforced unsubscribe at the observer's teardown path - pick one and verify it in a test, don't leave it as a convention.

Mediator: sound pattern, one specific, well-named failure mode

Symmetric to Observer: Mediator is not obsolete (centralized state managers - Redux, Zustand - are Mediator), but it has exactly one well-documented way to fail: the mediator accretes actual business logic instead of pure routing/coordination logic, at which point it is a God Object with extra ceremony. A widely circulated engineering critique of the popular .NET MediatR library ("MediatR is an anti-pattern," gist by TrevorDArcyEvans) makes this concrete at production scale: routing every operation through a generic mediator can obscure direct, traceable call paths behind indirection that exists only to satisfy the pattern, not a real multi-party coordination need.

Decision rule: if a Mediator's notify/handle method contains anything beyond "call method X on component Y based on this event," the logic belongs in a service the mediator calls, not inline in the mediator.

Chain of Responsibility: sound pattern, the debuggability tax is real and named

No source argues Chain of Responsibility is obsolete - it's the literal shape of every HTTP middleware stack in production. The convergent, repeated criticism across independent write-ups is operational, not architectural: when a request produces the wrong result, tracing which handler in the chain acted (or silently declined) is harder than reading one if/elif block, because the control flow is not visible at any single read site.

Decision rule: every handler in a chain logs its own entry/decision (act, pass, short-circuit) at the point it makes that decision; chain assembly happens once, explicitly, at the composition root - never build the chain via runtime auto-discovery/reflection, which hides ordering mistakes that are otherwise a five-second code read to find.

Decision Table: Pattern Selection by Context

SituationRecommendation
Stateless algorithm variation, language has closuresPlain function parameter - do not build a Strategy hierarchy
Action needs undo, replay, or queuing across a process boundaryCommand object with execute/undo, not a closure (closures don't serialize)
Fixed, closed set of types; operation dispatches per typeSealed type + exhaustive match/switch, not Visitor, if the language supports it
Open-ended, unbounded set of types (plugins, user-defined nodes)Visitor remains the correct choice - sealed types cannot express an open set
Shared step ordering across variants, real state threading between stepsTemplate Method is justified; if it's one step, pass a function instead
Long-lived subject, short-lived observersObserver with weak references or enforced unsubscribe - audit this explicitly
More than ~4 tightly coupled peer componentsMediator, kept to pure routing - push logic to services it calls
Configurable, ordered set of independent request handlersChain of Responsibility, with per-handler logging and explicit assembly
Collection traversal, any size, in a language with generators/iteratorsUse the language's native iterator protocol; do not hand-build an Iterator class

Sources