The idea
What it is
You've now met a shelf full of design patterns. Here's the lesson that ties them together: the goal was never to use patterns — the goal is simple code that solves the problem. A pattern is a tool for removing a specific, real pain: a change that keeps rippling through your code, a piece you genuinely need to swap out, a class that's grown into a monster. When that pain is present, the right pattern is a relief. When it isn't, the same pattern is pure cost — more files, more indirection, more things a new reader has to hold in their head — for nothing in return.
Reaching for a pattern before the pain arrives has names: over-engineering and speculative generality. It's building for a future that may never come — a direct violation of YAGNI, "You Aren't Gonna Need It" ([[dry-kiss-yagni]]). The tell is a justification that starts with "what if one day we need to...". Most of those days never come, and you pay the carrying cost of the abstraction every single day in between.
So flip the instinct. Reach for the simplest thing that works first. Write the plain function, the plain class, the direct call. Then let patterns emerge: when duplication shows up, when a change forces edits in five places, when a class is clearly doing three jobs — that's the signal. You refactor to the pattern to relieve pain you can point at. Patterns are a destination you arrive at, not a starting line you draw first.
The one sentence to remember
Don't apply a pattern to a problem you don't have yet — reach for the simplest thing that works, and let the pattern emerge through refactoring the moment a real, present pain appears.
A pattern for its own sake is a smell
If you can't name the concrete pain a pattern removes right now — the exact change that keeps rippling, the swap you actually make — you're not designing, you're decorating. "We used the Strategy pattern" is not an achievement. "This value now varies without touching the caller" is.
Mechanics
How it works
A gallery of anti-patterns
An anti-pattern is a solution that looks reasonable, gets used a lot, and reliably makes things worse. Most pattern misuse falls into a handful of recognizable shapes. Learn to spot these on sight — in your own diffs and in code review.
1 · Singleton abuse — a global variable in a tuxedo
The [[singleton]] gives you one instance with a global access point — and that global access is exactly the trap. Any code, anywhere, can call Config.getInstance(), so dependencies become invisible: a class secretly reaches out and grabs what it needs instead of being handed it. That makes it hard to test (you can't swap in a fake), hard to reason about (who touches this?), and quietly couples half your codebase to one object.
// SMELL: hidden dependency, global reach-out, untestable
class OrderService {
save(o: Order) {
Database.getInstance().insert(o); // where did this come from?!
}
}
// FIX: inject it — the dependency is now visible and swappable
class OrderService {
constructor(private db: Database) {} // handed in, not grabbed
save(o: Order) { this.db.insert(o); } // a fake DB slots in for tests
}The fix is almost always the same: inject the dependency instead of grabbing a global ([[dependency-injection-and-ioc]]). You keep "one instance" if you truly need it — you just pass it in rather than letting every class summon it from the ether.
2 · Speculative generality — abstraction nobody asked for
This is the purest form of pattern overuse: an interface with exactly one implementation, a Factory that only ever builds one type, a base class with a single subclass, "configurable" knobs no caller ever turns. It was all added "in case we need it later." Every one of these is a layer of indirection you pay for now — more files to open, more hops to trace — buying flexibility you aren't using.
interface IPriceCalculatorwith one classPriceCalculator— the interface is dead weight until a second implementation exists.- A
WidgetFactory.create()that always returnsnew StandardWidget()— the factory adds a hop and decides nothing. - A method that takes a
strategyparameter that's always the same value at every call site.
The fix is a delete key
Speculative generality is the one smell you fix by removing code. Collapse the one-impl interface into the concrete class, delete the factory and call new Thing(), drop the unused parameter. When the second case genuinely arrives, re-introducing the abstraction is a quick, well-informed refactor — and now you'll shape it to the two real cases instead of an imagined one.
3 · The God object — one class that does everything
A Manager or Utils or Application class that grows to 2,000 lines and touches everything: parsing, validation, database, formatting, networking. It violates the Single Responsibility Principle ([[single-responsibility]]) — it has a dozen reasons to change, so every change risks breaking something unrelated. Nobody can hold it in their head, and two people can't work near it without colliding. Fix: split it along its responsibilities into small classes that each do one thing.
4 · Poltergeist — the class that just forwards
A Helper, Wrapper, or Manager whose every method does nothing but call another object's method and return the result. It adds a name and a file but no behavior — a ghost in the call stack. You trace a call through three classes only to find the third one does the actual work. If a layer only forwards, delete it and call the real thing directly.
5 · Golden hammer / pattern-itis — forcing a favorite
You just learned Strategy, so now everything is a Strategy — even a tax rate that has been 0.2 since the company was founded. This is the "when you have a hammer, everything's a nail" trap: applying one beloved pattern everywhere regardless of fit. Its worst form is the AbstractFactoryFactory — patterns stacked on patterns until the ceremony dwarfs the work. A constant doesn't need a Strategy. A single object doesn't need a Factory. Match the tool to the force, not to your enthusiasm.
6 · Event soup — premature Observer / indirection overload
Everything fires events; everything listens; and now no one can answer "what happens when I click Save?" by reading the code — the flow scatters across a dozen handlers that trigger each other. [[observer]] and event buses are powerful, but heavy indirection has a cost: control flow becomes invisible. Use direct calls until decoupling buys you something real (a truly varying, runtime set of listeners). If a straight-line function would do, write the straight-line function.
How to apply patterns wisely
- Start simple. Write the most direct code that solves today's problem — the plain function, the direct call, the concrete class. Ship it.
- Wait for the pain. Let duplication or a rippling change reveal itself. The rule of three: the first time, just write it; the second time, note the duplication; the third time, refactor.
- Refactor TO the pattern. When the pain is real and repeatable, introduce the pattern to relieve it — shaped by the actual cases you now have, not guesses.
- Prefer the smallest pattern. A function beats a Strategy class; a plain object beats a Factory. Choose the least machinery that removes the pain.
- Delete abstractions that never earned a second implementation. An interface, factory, or base class that's had one concrete use for a year isn't flexibility — it's cost. Collapse it.
// OVER-ENGINEERED: a pattern for a value that never varies
interface IGreeter { greet(name: string): string; }
class EnglishGreeter implements IGreeter {
greet(name: string) { return "Hi " + name; }
}
class GreeterFactory {
static create(): IGreeter { return new EnglishGreeter(); } // only one!
}
const msg = GreeterFactory.create().greet("Ada"); // 3 files, 1 behavior
// HONEST: the whole job, no ceremony
function greet(name: string) { return "Hi " + name; }
const msg2 = greet("Ada"); // 1 line, same resultThe mature move is often not to add anything
By the end of the patterns phase, the sign you've learned patterns isn't how many you can name — it's how confidently you can look at a clean three-line function and say "this is done, it doesn't need a pattern." Knowing when not to reach for one is the whole skill.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A decision bench for one tiny requirement: "greet a user." On the left, the 🧩 over-engineered build — a GreeterFactory, an IGreeter interface, a GreetingStrategy, five files. On the right, the ✅ simple build — one three-line function. A complexity meter shows the left side sky-high and the right side at 1. Below, flip the "New requirement?" chips. While no real varying force is on, the verdict reads ✅ Keep it simple — YAGNI and the whole left column is stamped WASTE. Flip on a genuine force — "greetings vary by language at runtime" — and the verdict flips to Now a Strategy earns its keep and the pattern turns green. Same pattern, waste or wisdom, decided entirely by whether a real force is present.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
See the same job built two ways
Look at the bench with no requirement chips flipped. Left: the 🧩 over-engineered greeter — GreeterFactory → IGreeter → GreetingStrategy, five files, complexity meter pinned high. Right: the ✅ simple greet(name) function, complexity meter at 1. Read the verdict: ✅ 'Keep it simple — YAGNI', and the whole left column stamped WASTE. Same output, wildly different cost — because right now, nothing varies.
Add fake requirements — nothing changes
Flip on the soft chips: '☐ Might need more languages someday' and '☐ Only ever one greeting'. Watch the verdict: it stays ✅ 'Keep it simple' and the left column stays WASTE. A 'what if someday' is not a present force — it's speculative generality. The pattern still earns nothing. This is YAGNI in action: a maybe-future doesn't justify indirection today.
Add a REAL force — the pattern earns its keep
Flip on '☑ Greetings vary by language at runtime'. Now the verdict flips to 'Now a Strategy earns its keep', the left column turns green (JUSTIFIED, not WASTE), and the complexity is finally paying for something the problem actually demands. The pattern didn't change — the presence of a real, varying force did. Hit ↺ Reset and note how the same structure snaps back to WASTE the instant the real force is gone.
In practice
When to use it — and what trips people up
Signs you actually NEED a pattern...
- A change keeps rippling — the same edit forces you to touch three, four, five places every time. That duplication of change is the clearest call for structure.
- You genuinely swap implementations — there are already two or more real variants (payment providers, export formats, storage backends), not one plus a hypothetical.
- A class has grown many reasons to change — it's clearly doing several jobs and every edit is scary. Splitting along responsibilities is overdue ([[single-responsibility]]).
- You hit the pattern's exact problem — you can name the force in one sentence: "this must vary at runtime" ([[strategy]]), "one object changes and many must react" ([[observer]]).
Signs you're over-engineering...
- Your reason starts with "what if someday..." — you're building for an imagined future, not a present pain. That's speculative generality, a YAGNI violation.
- An interface, factory, or base class has exactly one implementation — and has for a while. It's indirection with no payoff; collapse it.
- You're reaching for a pattern because you just learned it — the golden-hammer reflex. Fit the tool to the force, not to your excitement.
- A reader can't trace the flow — Managers forward to Helpers that fire events into a bus. If the indirection buys nothing, the straight-line version is better code.
What it gives you
- Right pain, right tool — a pattern used to relieve a real, present force removes duplication and makes the next change local and safe.
- Shared vocabulary — 'this is a Strategy' tells a teammate the shape of the code in three words, when the shape is actually there.
- Emergent, well-shaped design — patterns arrived at through refactoring fit the real cases you have, not the guesses you started with.
- Confident restraint — the discipline to write a clean three-line function and leave it alone is itself a mark of design maturity.
Common mistakes
- Dead-weight indirection — a one-implementation interface or forwarding wrapper adds files and hops that buy nothing and slow every reader.
- Higher cognitive load — speculative abstractions make people trace calls through layers to find where the actual work happens.
- Harder change, not easier — the wrong or premature abstraction has to be unwound before you can make the change it didn't anticipate.
- Hidden coupling and lost flow — Singleton globals and premature event soup make dependencies invisible and control flow untraceable.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
// ❌ OVER-ENGINEERED — a Factory + interface + Strategy for a value
// that never varies. Five moving parts, one behavior.
interface IGreeter { greet(name: string): string; }
interface GreetingStrategy { format(name: string): string; }
class DefaultStrategy implements GreetingStrategy {
format(name: string) { return "Hi " + name; }
}
class Greeter implements IGreeter {
constructor(private strategy: GreetingStrategy) {}
greet(name: string) { return this.strategy.format(name); }
}
class GreeterFactory {
static create(): IGreeter { return new Greeter(new DefaultStrategy()); }
}
const message = GreeterFactory.create().greet("Ada");
// ✅ HONEST — the exact same result, no ceremony.
function greet(name: string) {
return "Hi " + name;
}
const message2 = greet("Ada");References & further reading
6 sources- Docsen.wikipedia.org
Anti-pattern — Wikipedia
The concept itself: what makes a common 'solution' reliably counterproductive, with a catalogue spanning software, organizations, and methodology. Good orientation.
- Articlemartinfowler.com
Yagni — Martin Fowler
The definitive short essay on 'You Aren't Gonna Need It' — why building for imagined futures (speculative generality) carries real cost you pay every day. The heart of this lesson.
- Articlemartinfowler.com
Is Design Dead? — Martin Fowler
On evolutionary design: let structure emerge through refactoring rather than front-loading patterns. Argues directly for arriving at patterns instead of starting with them.
- Booken.wikipedia.org
AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis — Brown, Malveau, McCormick, Mowbray
The 1998 book that popularized the term — origin of the Golden Hammer, Poltergeist, God object (Blob), and Spaghetti Code, each with a refactored fix.
- Articlesourcemaking.com
Singleton — SourceMaking (and its criticism)
A clear walkthrough of Singleton that also lays out why it's so often an anti-pattern: global state, hidden dependencies, and the testing pain that follows.
- Articlesourcemaking.com
AntiPatterns catalogue — SourceMaking
A browsable gallery of software-development, architecture, and project-management anti-patterns — handy for putting a name to a smell you keep seeing.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 06
When is the right time to introduce a design pattern?
question 02 / 06
What is 'speculative generality'?
question 03 / 06
Why is Singleton so often called an anti-pattern?
question 04 / 06
What does the 'rule of three' advise about duplication and abstraction?
question 05 / 06
What's the cost of an interface (or factory) that has exactly one implementation?
question 06 / 06
A teammate wraps a constant tax rate (0.2, unchanged for years) in a full Strategy pattern 'for flexibility.' Which anti-pattern is this?
0/6 answered