--- title: "Behavioral Design Patterns: Documented Obsolescence and Decision Rules" description: "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." language: null framework: null category: design_patterns tags: - design-patterns - behavioral-patterns - critique - over-engineering - closures - pattern-matching keywords: - strategy pattern vs closures - visitor pattern pattern matching sealed classes - command pattern lambda replacement - observer pattern lapsed listener memory leak - mediator pattern god object - template method fragile base class - norvig design patterns dynamic languages last_updated: "2026-07-08" difficulty: intermediate related: - ../principles/solid.md - ../principles/dry_kiss_yagni.md - ./creational.md - ./structural.md - ../architecture/clean_architecture.md search_priority: high status: published --- # Behavioral Design Patterns: Documented Obsolescence and Decision Rules 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: 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 | 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](../principles/solid.md) - the same over-abstraction failure mode (indirection without a named beneficiary) recurs across both documents. - [Creational Design Patterns](./creational.md) - Command/Strategy objects are frequently produced by a Factory; the same closure-first bias applies there. - [Structural Design Patterns](./structural.md) - 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`](https://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`](https://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`](https://tahazsh.com/blog/strategy-pattern-with-functions/). - Robert Nystrom, *Game Programming Patterns*, "Command" chapter - [`gameprogrammingpatterns.com/command.html`](https://gameprogrammingpatterns.com/command.html) - and "Closures and the Command Pattern" - [`journal.stuffwithstuff.com/2009/07/02/closures-and-the-command-pattern`](https://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`](https://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`](https://puredanger.github.io/tech.puredanger.com/2007/07/03/pattern-hate-template/). - Wikipedia, "Lapsed listener problem" - [`en.wikipedia.org/wiki/Lapsed_listener_problem`](https://en.wikipedia.org/wiki/Lapsed_listener_problem). - Robert Nystrom, *Game Programming Patterns*, "Observer" chapter - [`gameprogrammingpatterns.com/observer.html`](https://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`](https://gist.github.com/TrevorDArcyEvans/ae7dfaf20e4cae1294fb74e9c4efb68b) - production-scale critique of over-applied Mediator.