Every Pattern Has a Failure Mode
Read the GoF Consequences section first.
Singleton is Global State. Visitor is “I gave up on polymorphism.” Observer is action at a distance. Each GoF pattern has a Consequences section that names the cost; read it before reaching for the pattern, not after.
Opinion
The Gang of Four book's most under-read part is the Consequences section at the back of every pattern.1Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). Each pattern's Consequences section names the trade-offs honestly. Most readers stop at Intent / Motivation; the bodies are buried at the back. Intent / Motivation / Applicability / Structure get all the airtime; the Consequences section, where Gamma et al. honestly list what each pattern costs you, is where the bodies are buried. Read seriously, the Consequences sections are a forty-year-old catalogue of anti-patterns hiding in plain sight.
Singleton is the cleanest example. The Singleton Consequences section in GoF names: violates single-responsibility (the class manages both its single-instance constraint and its actual job), introduces hidden state, makes testing harder.2Singleton Consequences (op. cit., page 130). Lists violations of single-responsibility, hidden state, and testability cost. The catalogue authors documented the failure mode from page one. Misko Hevery wrote the canonical critique in 2008 (“Root Cause of Singletons”): a Singleton is Global State wearing a class costume.3“Root Cause of Singletons” (Google Testing Blog, 2008). The canonical modern critique: a Singleton is Global State; the testability cost is real; the mitigation is constructor injection. Erich Gamma himself said in 2009 he would remove Singleton from a revised GoF.4Larry O'Brien interviewing Erich Gamma (InformIT, 2009). Asked what he would change in a revised GoF, Gamma names Singleton as the pattern he would remove. Wikipedia's Design Patterns page summarises. The pattern hasn't changed; the consensus has caught up to the Consequences section that was always there.
Visitor is the same shape with a different audience. Steve Yegge's “When Polymorphism Fails” nails the diagnosis: Visitor exists because OO cannot double-dispatch.5“When Polymorphism Fails” (steveyegge2 site, 2008). Visitor exists because OO can't double-dispatch; reaching for Visitor is admitting the type system can't carry the constraint. In a sum-type language, fix the types. Reaching for Visitor is admitting the type system cannot carry the constraint, and often the right move is to fix the types (sum types, pattern matching, a sealed hierarchy) rather than reach for the pattern. Observer is the action-at-a-distance trap: excellent for decoupling, a debugging nightmare. Refactoring Guru's own Cons section is honest about it.6Observer Cons section (refactoring.guru/design-patterns/observer). Subscribers receive notifications in random order; the chain of cause-and-effect is hard to trace. The action-at-a-distance failure mode named plainly.
Peter Norvig's 1996 essay extends the argument: sixteen of the twenty-three GoF patterns become invisible or simpler in a dynamic language with first-class functions.7“Design Patterns in Dynamic Languages” (1996). Sixteen of twenty-three GoF patterns become invisible or simpler in a language with first-class functions and dynamic dispatch. The implicit cost: language ceremony you don't need. Factory Method becomes a function. Strategy becomes a function. Command becomes a closure. The Consequences sections of those patterns include a hidden cost Norvig names out loud: in the wrong language, the pattern is ceremony with no payload. The discipline is not to reach for the pattern just because the catalogue offers it; reach for it when the cost listed in the Consequences section is a cost worth paying.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
Before reaching for a pattern, name its failure mode. Singleton is Global State (mitigate with constructor injection). Visitor is polymorphism surrender (prefer sum types). Observer is action at a distance (mitigate with tracing). Read the GoF Consequences section first. /tenet/every-pattern-has-a-failure-mode/PT3
AI eyes only
Rule: when introducing a pattern, name its failure mode in the same paragraph.
Reject: introducing Singleton, Observer, Visitor, etc. without naming the cost. Reject: reaching for a pattern because it “looks like senior code”.
Generate: when introducing a pattern, write the cost and the mitigation alongside it. Example: “Singleton here; cost is hidden state and harder testing; mitigation is constructor injection at the boundary.”
Diagnostic: every pattern introduction must include a “cost: …; mitigation: …” line. No cost named, no pattern.
Why?
- Turns the GoF Consequences section into a checklist. Each pattern has a documented cost list; PT3 makes reading it the cost of admission to using the pattern.
- Names each pattern's failure mode out loud: Singleton ↔ Global State, Visitor ↔ Polymorphism Surrender, Observer ↔ Action at a Distance, Factory ↔ Yet Another Indirection. Reviewers can point at the failure mode by name.
- For Visitor specifically, prefers the type-system answer over the pattern. Sealed sum types and exhaustiveness checking solve the original problem at compile time. Pairs with T1 Domain-Driven Types.
- For Singleton, names the mitigation alongside the pattern: constructor injection at the boundary. Hidden state becomes explicit dependency; the testability cost evaporates.
- Reads the Consequences section as a 2026 checklist, not a 1994 verdict. Some costs aged out (Singleton thread safety in Node); some didn't (Singleton hidden state in any language). The discipline is judging which apply.
- The CLAUDE.md instruction “name the failure mode in the same paragraph as the pattern” turns agent pattern-fluency into honesty. The agent has read the Consequences section; PT3 asks it to remember.
- Brown et al.'s AntiPatterns book is the systematic companion to GoF — forty entries pairing the success name with the failure name. PT3 makes the pairing the unit of decision.
Origins
The Consequences section was in the GoF book from page one in 1994.1Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). Each pattern's Consequences section names the trade-offs honestly. Most readers stop at Intent / Motivation; the bodies are buried at the back. Each of the 23 patterns ends with a numbered list of trade-offs — what you give up to get the pattern's benefits. Singleton: hard to test, hidden state, single-responsibility violation. Visitor: hard to add new Element types. Observer: cyclic dependencies, unexpected updates, debug difficulty. The catalogue authors were honest about the costs from the start; the industry mostly ignored that section.
The first popular critique landed twenty years later. Misko Hevery's 2008 Google Testing Blog post “Root Cause of Singletons” named the failure mode in modern working-developer terms: a Singleton is Global State, the testability cost is real, the mitigation is constructor injection.3“Root Cause of Singletons” (Google Testing Blog, 2008). The canonical modern critique: a Singleton is Global State; the testability cost is real; the mitigation is constructor injection. The post became the canonical reference for “Singleton is an anti-pattern, actually” and arguably ended the pattern's career as a default OO design move.
Erich Gamma confirmed the consensus in a 2009 InformIT interview with Larry O'Brien: asked what he would change in a revised GoF, Gamma named Singleton as the pattern he would remove.4Larry O'Brien interviewing Erich Gamma (InformIT, 2009). Asked what he would change in a revised GoF, Gamma names Singleton as the pattern he would remove. Wikipedia's Design Patterns page summarises. The pattern hasn't changed; the consensus has caught up to the Consequences section that was always there.
Steve Yegge's “When Polymorphism Fails” (2008) extended the same argument to Visitor.5“When Polymorphism Fails” (steveyegge2 site, 2008). Visitor exists because OO can't double-dispatch; reaching for Visitor is admitting the type system can't carry the constraint. In a sum-type language, fix the types. Visitor exists because OO can't double-dispatch; reaching for Visitor is admitting the type system can't carry the constraint. In a language with sealed sum types, the right move is to fix the type system. Yegge's broader “Execution in the Kingdom of Nouns” (2006) made the cultural case: pattern-as-class-hierarchy is one bar of the noun-cage.9“Execution in the Kingdom of Nouns” (2006). The cultural critique of treating every concept as a noun-class; pattern-as-class-hierarchy is one bar of the noun-cage. PT3 reads as: name the cost of the bar before adding it.
Brown, Malveau, McCormick and Mowbray's AntiPatterns book (1998) is the systematic catalogue of the failure-mode pairings.10AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis (Wiley, 1998). The systematic catalogue of failure-mode pairings; forty entries, each with a success name and a corresponding anti-pattern. Forty entries, each with a name on the success side and a corresponding anti-pattern entry on the failure side. The book is dated in places — some entries have aged poorly — but the editorial move (every pattern has an anti-name) is exactly what PT3 names. Pair it with the GoF Consequences sections; the two together cover most of the pattern-as-failure-mode discipline.
Peter Norvig's 1996 essay closes the loop from a different direction.7“Design Patterns in Dynamic Languages” (1996). Sixteen of twenty-three GoF patterns become invisible or simpler in a language with first-class functions and dynamic dispatch. The implicit cost: language ceremony you don't need. Sixteen of the twenty-three GoF patterns become invisible or simpler in a dynamic language with first-class functions. The implicit Consequences-section cost in those cases is “language ceremony you don't need.” Norvig wasn't anti-pattern; he was naming the cost the catalogue didn't name out loud.
Quotes
It can make unit testing more difficult, since the Singleton class typically holds onto state across tests, and the application code has no way to substitute a different implementation.
Singletons are nothing more than global state. Global state makes your code shorter, easier to write — and impossible to test, reason about in isolation, or change without breaking other things.
Visitor is what you reach for when polymorphism stops being able to do the job — which means you should be asking why polymorphism stopped working before you reach for Visitor.
Sixteen of the twenty-three patterns are either invisible or simpler in Lisp than in C++, for at least some uses of each pattern. The patterns are language-shaped — and the cost of using them is language-shaped too.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01Each of the 23 patterns ends with a numbered list of trade-offs — what you give up to get the benefits. Singleton: hard to test, hidden state. Visitor: hard to add new Element types. Observer: cyclic dependencies, debug difficulty. The catalogue authors were honest from page one; PT3 asks readers to remember.
- 02“Root Cause of Singletons”SupportsThe canonical modern critique. A Singleton is Global State wearing a class costume; the testability cost is real and the mitigation is constructor injection. Effectively ended Singleton's career as a default OO design move.
- 03“When Polymorphism Fails”SupportsVisitor exists because OO can't double-dispatch; reaching for Visitor admits the type system can't carry the constraint. In a sum-type language the right move is to fix the types. PT3 lifts this for the Visitor failure mode.
- 04Sixteen of the 23 GoF patterns become invisible or simpler in a language with first-class functions and dynamic dispatch. The implicit cost the catalogue doesn't name: language ceremony you don't need. Norvig isn't anti-pattern; he is naming the cost out loud.
- 05Subscribers receive notifications in random order; the chain of cause-and-effect is hard to trace. Refactoring Guru's Cons section is the modern statement of the action-at-a-distance failure mode.
Sixteen sources clustered tightly around the four named failure modes. The canonical critiques sit at the top: Gamma et al. on the Consequences sections, Hevery on Singleton, Yegge on Visitor, Norvig on dynamic-language patterns, plus Refactoring Guru's own Cons sections. The qualifiers further down are the catalogue authors honestly naming costs. The opposers are the “but sometimes a Singleton is fine” literature; the reply argues the rule is not against patterns, it is against unconscious adoption.
Examples
// Before: Singleton hides global state; no seam for a test fake.export class HedgehogTracker { static instance = new HedgehogTracker(); private sightings: Sighting[] = []; record(sighting: Sighting): void { this.sightings.push(sighting); }}HedgehogTracker.instance.record(sighting);
// After: tracker is injected; the test passes a fake.export class HedgehogTracker { private sightings: Sighting[] = []; record(sighting: Sighting): void { this.sightings.push(sighting); }}export class Patrol { constructor(private readonly tracker: HedgehogTracker) {} log(sighting: Sighting): void { this.tracker.record(sighting); }}
AI rules
.cursor/rules/pt3-every-pattern-has-a-failure-mode.mdc---
description: Prickles PT3 — Every Pattern Has a Failure Mode
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## Prickles PT3 — Every Pattern Has a Failure Mode
Before introducing a pattern, read its Consequences (or Cons) section in GoF / Refactoring Guru / the canonical reference. Name the cost out loud in the same paragraph as the choice.
For Singleton, name the mitigation: constructor injection at the boundary, never module-level `getInstance()`. Hidden state becomes explicit dependency.
For Visitor, prefer the type-system answer when the language supports it: sealed sum types, discriminated unions, exhaustiveness checking. Visitor is the fallback for languages that can't double-dispatch.
For Observer, document the cause-and-effect chain. Action at a distance is the documented failure mode; mitigate with a tracing layer or named events that survive grep.
Refuse to ship a pattern without naming its failure mode. The Consequences section was honest in 1994; the agent has read it; the rule is to remember.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The honest steelman is that some patterns' Consequences sections name costs that are overblown for modern codebases. The 1994 Singleton Consequences section worried about thread safety in C++, mostly irrelevant in a single-threaded JavaScript runtime. The Visitor Consequences section worried about the cost of adding new element types, mostly solved by sealed sum types in a language with discriminated unions. Treating the Consequences section as gospel risks reading 1994 trade-offs as 2026 ones, and PT3's “every pattern has a failure mode” line risks calcifying that misreading. The honest counter: read the Consequences section and ask whether the cost still applies in the current language and runtime.
The second steelman is Kerievsky's Refactoring to Patterns + Fowler's rule of three: do not reach for a pattern until the third caller arrives, because earlier pattern-naming risks the wrong abstraction. The argument: read the Consequences section, then wait for three identical-shaped uses before naming the pattern.
Counter-argument retort
The counter is right that some 1994 costs are 2026 non-issues. The reply is to read the Consequences section as a checklist, not a verdict. Singleton's 1994 thread-safety worry is irrelevant in single-threaded Node.js; Singleton's 1994 hidden-state worry is exactly as relevant now — more so as dependency-injection conventions have hardened around it. The discipline isn't “refuse the pattern because the catalogue named a cost”; it's “name which Consequences items still apply, and decide consciously.”
The Hevery objection3“Root Cause of Singletons” (Google Testing Blog, 2008). The canonical modern critique: a Singleton is Global State; the testability cost is real; the mitigation is constructor injection. survives every modernisation of the Consequences section. Hidden state and hard testing are language-independent costs of Singleton; the cost shows up in TypeScript exactly as it shows up in Java. Constructor injection at the boundary — pass the dependency in, store it on the object, never reach for a module-level getInstance() — is the mitigation, and it is also the answer in every language. PT3 doesn't forbid Singleton; it asks you to name the cost and the mitigation in the same paragraph as the choice.
The Yegge objection5“When Polymorphism Fails” (steveyegge2 site, 2008). Visitor exists because OO can't double-dispatch; reaching for Visitor is admitting the type system can't carry the constraint. In a sum-type language, fix the types. for Visitor cuts the same way. In a language with sealed sum types and pattern matching — Rust, modern TypeScript with discriminated unions, F#, Scala, Kotlin — Visitor's 1994 cost (must touch every Element when adding a new operation) is replaced by exhaustiveness checking, which catches the same bug at compile time. So PT3 in a 2026 sum-type-rich language reads: prefer the type-system answer; reach for Visitor only when the language genuinely doesn't support double-dispatch. The discipline updates with the tooling; the “read the Consequences section” rule doesn't.
The agent dimension is the closing argument. Agents trained on legacy corpora reach for Singleton because the corpora are full of Singletons; the failure mode is the agent shipping the 1994 cost into a 2026 codebase without naming it. The CLAUDE.md instruction that PT3 lives behind — name the failure mode in the same paragraph as the pattern — turns the agent's pattern-fluency into a feature instead of a bug. The agent has read the Consequences section; the rule asks it to remember.
The rule-of-three steelman (Kerievsky / Fowler / Beck — wait for three callers before naming a pattern) is acknowledged and refused. Don't wait for three. The Fowler/Beck timing rule is an artefact of the pre-AI era when the cost of detecting and renaming an emerging pattern was high. With agents reading the diff before the human does, the cost of an early pattern name is lower than the cost of three near-identical Strategies drifting independently for two sprints. Name the pattern when the second caller appears and the Consequences section's costs apply; if a third caller reshapes the abstraction, the agent renames it. The wrong pattern name is cheap to fix; the duplicated near-Strategy you missed is not.
Notes
- [1]Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). Each pattern's Consequences section names the trade-offs honestly. Most readers stop at Intent / Motivation; the bodies are buried at the back.
- [2]Erich Gamma et al. — Singleton Consequences (op. cit., page 130). Lists violations of single-responsibility, hidden state, and testability cost. The catalogue authors documented the failure mode from page one.
- [3]Misko Hevery — “Root Cause of Singletons” (Google Testing Blog, 2008). The canonical modern critique: a Singleton is Global State; the testability cost is real; the mitigation is constructor injection.