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:
| Pattern | Eliminated/simplified by | Mechanism |
|---|---|---|
| Command, Strategy, Template Method, Visitor | First-class functions | Pass a function/closure instead of instantiating a single-method interface |
| Iterator, Interpreter | Macros / language-level generators | The language already provides lazy sequence traversal |
| Mediator, Observer | Method 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
| Pattern | 2020s verdict | Replace with | Named source |
|---|---|---|---|
| Strategy | Legacy ceremony for the stateless case | Plain function/closure parameter | Norvig 1996; Shashtari, "Strategy pattern is better when done with functions" |
| Command | Legacy ceremony unless undo/queue/audit is a real requirement | Closure/lambda; keep the class only for serializable or undoable actions | Nystrom, Game Programming Patterns ("Command" chapter); journal.stuffwithstuff.com, "Closures and the Command Pattern" |
| Visitor | Actively being replaced in the languages that popularized it | Sealed types/enums + exhaustive switch/pattern matching | nipafx, "Visitor Pattern Considered Pointless"; JEP 409 (sealed classes) + JEP 441 (pattern matching for switch), Java 21 |
| Template Method | Named "hate" target; fragile-base-class failure mode is well documented | Composition: pass the varying step in as a function/strategy, not an override point | Miller, "Patterns I Hate #2: Template Method"; standard composition-over-inheritance literature |
| State | Sound pattern; verdict depends on language, not era | Sum type/enum + exhaustive match in languages that have them; keep the class form in languages that don't | Norvig 1996 (first-class types); general ADT literature |
| Observer | Sound pattern; the specific documented failure is memory retention, not obsolescence | Same pattern, but with weak references or explicit unsubscribe/dispose, or move to a broker at scale | Wikipedia, "Lapsed listener problem"; gameprogrammingpatterns.com/observer.html |
| Mediator | Sound pattern; specific documented failure is scope creep into a God Object | Same pattern, but mediator handles routing only, never business logic | slidetodoc.com summary of GoF Mediator; God Object literature (Riel) |
| Chain of Responsibility | Sound pattern (middleware is this, everywhere); documented cost is debuggability | Same pattern, but log entry/exit per handler and keep chain assembly visible at the composition root, not dynamically discovered | Multiple production write-ups converge on "hard to trace which handler acted" as the recurring complaint |
| Iterator | Fully absorbed into language runtimes | Native 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:
- The behavior carries configuration/state beyond its single call (
ExponentialBackoffPolicyholding tunable constants - a closure over those constants is equally valid and often shorter). - The object must satisfy a richer contract than "call it" - undo, serialize, replay, inspect without executing (Command's
execute/undopair 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
| Situation | Recommendation |
|---|---|
| Stateless algorithm variation, language has closures | Plain function parameter - do not build a Strategy hierarchy |
| Action needs undo, replay, or queuing across a process boundary | Command object with execute/undo, not a closure (closures don't serialize) |
| Fixed, closed set of types; operation dispatches per type | Sealed 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 steps | Template Method is justified; if it's one step, pass a function instead |
| Long-lived subject, short-lived observers | Observer with weak references or enforced unsubscribe - audit this explicitly |
| More than ~4 tightly coupled peer components | Mediator, kept to pure routing - push logic to services it calls |
| Configurable, ordered set of independent request handlers | Chain of Responsibility, with per-handler logging and explicit assembly |
| Collection traversal, any size, in a language with generators/iterators | Use the language's native iterator protocol; do not hand-build an Iterator class |
Related Documentation
- SOLID Principles: Documented Failure Modes and Decision Rules - the same over-abstraction failure mode (indirection without a named beneficiary) recurs across both documents.
- Creational Design Patterns - Command/Strategy objects are frequently produced by a Factory; the same closure-first bias applies there.
- Structural Design Patterns - Visitor is most often paired with Composite; the sealed-type replacement applies to that pairing too.
Sources
- Peter Norvig, "Design Patterns in Dynamic Programming" (Object World, 1996) -
norvig.com/design-patterns/design-patterns.pdf- the original 16-of-23-patterns-are-language-workarounds analysis. - Bafatakis et al., "On the Interaction of Object-Oriented Design Patterns and Programming Languages" (2019) -
arxiv.org/abs/1905.13674- independent, peer-reviewed confirmation of Norvig's thesis with a proposed language-construct catalog. - Taha Shashtari, "Strategy pattern is better when done with functions" -
tahazsh.com/blog/strategy-pattern-with-functions. - Robert Nystrom, Game Programming Patterns, "Command" chapter -
gameprogrammingpatterns.com/command.html- and "Closures and the Command Pattern" -journal.stuffwithstuff.com/2009/07/02/closures-and-the-command-pattern. - Nicolai Parlog (nipafx), "Visitor Pattern Considered Pointless - Use Pattern Switches Instead" -
nipafx.dev/java-visitor-pattern-pointless- cites JEP 409 (sealed classes) and JEP 441 (pattern matching for switch). - Alex Miller (puredanger), "Patterns I Hate #2: Template Method" -
puredanger.github.io/tech.puredanger.com/2007/07/03/pattern-hate-template. - Wikipedia, "Lapsed listener problem" -
en.wikipedia.org/wiki/Lapsedlistenerproblem. - Robert Nystrom, Game Programming Patterns, "Observer" chapter -
gameprogrammingpatterns.com/observer.html- practical account of the lapsed-listener cost in a shipped game engine. - TrevorDArcyEvans, "MediatR is an anti-pattern" -
gist.github.com/TrevorDArcyEvans/ae7dfaf20e4cae1294fb74e9c4efb68b- production-scale critique of over-applied Mediator.