- Design patterns are taught as universal solutions to recurring problems, but they are solutions to problems that do not exist in your codebase
- Patterns solve for elegance, abstraction, and theoretical purity โ but production solves for speed, debuggability, and understanding what is happening
- A pattern that is correct in isolation becomes problematic in context โ when you have 50 services, 10,000 lines of code, and people who have to understand it in a month
- The moment you apply a pattern, you trade clarity for indirection. The trade is only worth it if the problem is real enough and painful enough to justify the cost
I learned design patterns from a textbook in university.
The textbook showed a problem. A Singleton pattern. A Factory pattern. An Observer pattern. Each one solved a specific, abstract problem. The code was clean. Decoupled. Elegant.
Then I got to my first job and realized nobody was asking about abstract problems.
They were asking: why is this component so hard to test? Why do I have to change this file when I only touched that file? Why does this dependency flow hidden and cause crashes nobody can predict?
Design patterns were supposed to solve these problems. Sometimes they did. Often they made them worse.
Here is the uncomfortable truth: most design patterns are solutions to problems that do not exist in your codebase. You are solving for elegance. Production needs clarity.
The Gap Between Theory and Practice
A design pattern is taught like this:
"You have a problem. Components need to communicate without being tightly coupled. Solution: Observer pattern. You have observers. You have observables. When state changes, observers are notified. Elegant. Decoupled."
In a textbook, this is correct. It solves the problem it is supposed to solve.
But in production, you have a different problem:
"We have 50 components. A state change in component A triggers an observer in component B which triggers a side effect in component C. When component B changes and component C breaks, I have to trace through observer registrations that happen in constructors, setup methods, and lifecycle hooks spread across 10 files to understand what is happening."
The Observer pattern is now a liability. Not because it is a bad pattern. Because the problem it solved is not your actual problem.
Your actual problem is: I do not understand what happens when state changes. The pattern made that problem worse, not better.
What Patterns Actually Do
Design patterns are templates for solving abstract problems in abstract ways.
They prioritize:
- Decoupling
- Abstraction
- Elegance
- Theoretical correctness
- Extensibility for future use cases
What they do not prioritize:
- Clarity of what is happening now
- Debuggability
- Understandability for someone new to the codebase
- Simplicity
- Minimizing indirection
This is fine in textbooks. It is a problem in production.
A Factory pattern decouples object creation. This is elegant. But the developer reading the code now has to follow the abstraction to understand what object is actually being created. They have to read the Factory implementation. Then read the concrete implementations. Then understand the polymorphism.
A simple new UserRepository() does the same thing with no indirection. It is less elegant, but it is clear.
The question is: is the elegance worth the cost?
In a small codebase with one developer, it is not. In a large codebase where object creation is truly a pain point, it might be.
Most of the time, we apply patterns without answering this question. We apply them because they are elegant. Because they are taught. Because they look good in code reviews.s
The Consistency Problem
A design pattern is only useful if you apply it consistently.
A single Singleton is not too bad. You have one globally accessible instance. You know where it is. You can find it.
But Singletons breed. You have a logger Singleton. A config Singleton. A database connection Singleton. A cache Singleton. A service Singleton.
Now you have hidden global state everywhere. Your unit tests are failing because Singletons from a previous test are still alive. Your code is hard to trace because state changes invisibly from anywhere.
The pattern that looked elegant when you applied it to one problem became a nightmare when you applied it to five problems.
This is the consistency trap. Once you pick a pattern, you have to apply it everywhere to avoid cognitive dissonance. But applying it everywhere creates problems that applying it to one problem did not.
A strategy of "use Singletons for global state" works fine with one Singleton. It breaks when you have ten.
The textbook never explains this because textbooks show one pattern in isolation. Production has ten patterns interacting.
When the Pattern Becomes More Code Than the Problem
I watched a developer spend two days building a Decorator pattern for a logging system.
The pattern was elegant. It let you add logging to any method without modifying the method. Just wrap it. The wrapper logs. The decorator chain is clean.
In the end, the Decorator implementation was 300 lines. The logging it was adding was 50 lines.
A simple if statement would have done the same thing in 10 lines.
The pattern made the code harder to understand. It added indirection. It made the logging harder to test because you had to understand the Decorator chain.
The problem was not real enough to justify the pattern. The problem was "we want logging in this function." The solution was "add a log statement." The pattern was cargo cult.
But it looked good. It was elegant. It was extensible for future logging needs that would probably never happen.
That extensibility cost 300 lines of code for 50 lines of functionality.
The Truth About Extensibility
Design patterns prioritize extensibility. They solve for future use cases.
"This Factory pattern lets us easily add new implementations later without changing the client code."
This is true. It also means:
- You are writing code for a problem that does not exist yet
- You are adding indirection for a use case that might never happen
- You are betting that your prediction about the future is correct
- You are paying the cost today
The textbook says: "Good design should be extensible."
Production says: "Code that is easy to understand and change is more important than code that is hard to change because it is flexible for changes we have not thought of yet."
Most of the time, the future is not what you predicted. You will change the code anyway. The extensibility you built for will be wrong.
You could have written simple code. You could have changed it when the requirements changed. But instead you wrote flexible code for a future that did not arrive, and the flexibility became a burden.
What Actually Matters
The developers who understand design patterns well are not the ones who apply them everywhere.
They are the ones who:
- Recognize when the problem is real. Not theoretical. Real. "We have five implementations of this interface and they all have similar client code that needs to change together." That is when a pattern helps. "We might need five implementations someday" is not.
- Measure the cost of the pattern. How many lines of code? How much indirection? How hard is it to debug? Does the clarity cost more than the elegance gain?
- Know when to break the pattern. Sometimes the simplest code is the right code, even if it is less elegant. Production code that works is better than elegant code that is hard to understand.
- Understand that patterns are context-dependent. A pattern that is perfect in a 100,000-line codebase might be overkill in a 1,000-line script. A pattern that makes sense with five developers might be confusing with 50.
- Resist the urge to be clever. The textbook patterns are clever. But clever is not always right. Clear is right. Maintainable is right. Debuggable is right.
The Uncomfortable Truth
Most design patterns are applied to problems that do not exist.
A junior developer learns patterns. They love patterns. They see elegant solutions to abstract problems. They apply them everywhere. They think they are being a good engineer.
But they are not solving real problems. They are solving the textbook problems.
A senior developer knows the patterns. But they also know that the patterns are tools, not laws. They know that most code does not need the elegance that patterns provide. They know that simplicity is the enemy of bugs.
The developers who struggle the most with production code are often the ones who learned patterns well and never questioned them.
The developers who produce the best code are the ones who ask: "Is this problem real enough? Is the pain significant enough? Is the pattern worth the cost?"
And often the answer is no.