Loading learning content...
There's a peculiar disease that afflicts software developers after they've learned design patterns. It manifests as an irresistible urge to use them—not because the problem demands a pattern, but because the developer has a pattern looking for a problem.
This is Pattern for Pattern's Sake: the application of design patterns as an end in themselves, divorced from the genuine engineering needs they're meant to serve. It transforms design patterns from problem-solving tools into ritualistic complexity, applied to demonstrate knowledge rather than solve problems.
By the end of this page, you will understand the psychology behind unwarranted pattern application, recognize the signs of pattern worship in codebases and teams, develop rigorous criteria for when patterns are genuinely needed, and cultivate the confidence to choose simplicity over sophistication.
Pattern worship isn't about bad developers—it's about good developers with miscalibrated instincts. Understanding the psychology helps us recognize and correct this tendency in ourselves.
Pattern worship rests on several implicit beliefs, each superficially reasonable but ultimately misguided:
'Good code uses patterns.'
This inverts the actual relationship. Patterns are present in good code when they solve problems. The presence of patterns isn't an indicator of quality; the appropriate solving of problems is. Code without patterns that solves its problems elegantly is better than code with patterns that creates complexity.
'Patterns are best practices.'
Patterns are contextual practices. A pattern that's essential in one context is overhead in another. Factory Method is a best practice when you have polymorphic object creation; it's ceremony when you construct one type. 'Best practice' without context is meaningless.
'Simple code is junior code.'
This inverts years of hard-won wisdom. Simple code is expert code. The expert knows when to use patterns and when to refrain. The junior developer either knows no patterns (writing accidentally simple code) or knows patterns (and overuses them). The expert writes intentionally simple code.
'More abstraction is better.'
Every abstraction has a cost: indirection, cognitive load, barrier to understanding. Abstractions are justified only when they reduce net complexity—when the problem they solve is larger than the complexity they introduce.
The more patterns you know, the more restraint you need. Knowledge without discipline becomes a liability. The expert's power lies not in deploying patterns but in knowing when they're unnecessary—and having the confidence to embrace simplicity.
Let's be honest about an uncomfortable truth: pattern usage is a status marker. Using sophisticated patterns signals technical expertise. It marks you as someone who's 'read the books' and understands 'real' software engineering.
This creates perverse incentives:
In this environment, simplicity is punished. Writing straightforward code looks less sophisticated than writing patterned code, even when straightforward code is better. Developers—being rational actors in this status game—reach for patterns to signal competence.
Recognizing this dynamic is the first step to resisting it. The goal is to optimize for project success, not for appearing sophisticated.
Pattern for pattern's sake manifests in various forms, each with distinct characteristics. Recognizing these patterns (of anti-patterns) helps identify the problem in your codebase or team.
The first implementation is already abstracted behind interfaces, factories, and strategies—before any actual variation exists or is needed.
What it looks like:
// We have one payment method. Just one. Credit cards.
// Yet we've built:
interface PaymentStrategy { ... }
class CreditCardPaymentStrategy implements PaymentStrategy { ... }
class PaymentStrategyFactory { ... }
class PaymentContext { ... }
// When we could have:
class PaymentService {
async chargeCreditCard(card: CreditCard, amount: Money): Promise<Receipt> { ... }
}
The justification: 'We might add PayPal or Bitcoin later.'
The reality: When (if) those payment methods arrive, requirements will be clearer. The current abstraction, designed without knowledge of actual needs, may not fit. You'll refactor anyway—but with the burden of existing infrastructure.
The principle: Build concrete first, abstract when necessary.
Code is written to handle cases that don't exist, won't exist, and may never exist—all in the name of 'flexibility.'
What it looks like:
// A configuration system that handles all of:
// - Multiple serialization formats
// - Distributed configuration sources
// - Hot reloading
// - Schema validation
// - Migration between versions
// Actual requirements:
// - Read config.json on startup
The justification: 'This is how enterprise configuration systems work.'
The reality: You're not building an enterprise. You're building your specific project with your specific needs. Every unused capability is maintenance burden.
The principle: Build for requirements you have, not requirements you imagine.
The problem is reshaped to fit a pattern, rather than selecting a pattern (or no pattern) that fits the problem.
What it looks like:
Developer learns Observer pattern. Sees any communication between components. Forces it into Observer, even when direct method calls would be clearer. The notification system that notifies one thing. The event bus with one event type. The publisher with one subscriber.
The justification: 'This is the proper way to decouple.'
The reality: Decoupling has costs. Indirect communication is harder to trace, debug, and understand. Decoupling is justified when you need independent deployment, multiple subscribers, or genuine modularity. Decoupling for its own sake is indirection without benefit.
The principle: Let the problem shape the solution, not the pattern shape the problem.
When reviewing code, ask: 'What problem does this pattern solve?' If the answer references future possibilities, hypothetical extensions, or architectural purity—rather than current, concrete problems—you're looking at pattern for pattern's sake.
Every pattern carries costs. The decision to use a pattern should explicitly acknowledge and weigh these costs against the benefits—yet pattern worshippers often treat patterns as free.
| Cost Category | Description | Often Ignored Because... |
|---|---|---|
| Indirection | Code flow is harder to follow; behavior is spread across files | Developers know the pattern structure and don't see the barrier to others |
| Cognitive Load | Each abstraction is another concept to hold in memory | Original developers don't experience the learning curve |
| Code Volume | More classes, interfaces, files to maintain | IDE navigation makes it feel manageable |
| Type Ceremony | Interfaces, abstract classes, type parameters everywhere | Languages with good type inference reduce visible ceremony |
| Test Complexity | More components to mock, more integrations to verify | Test frameworks make boilerplate feel routine |
| Documentation Burden | Patterns require explanation; structure isn't self-evident | Original developers think 'everyone knows Factory pattern' |
The Cost-Benefit Framework:
Before applying any pattern, explicitly calculate:
COSTS:
BENEFITS:
THE RULE: Benefits must exceed costs by a significant margin. If it's close, prefer simplicity. Patterns that barely pay for themselves aren't worth the risk of being wrong.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
// QUESTION: Should we use Dependency Injection for this service? // ============================================// THE SERVICE WITHOUT DI// ============================================class OrderService { private db = new Database(); private emailClient = new EmailClient(); async createOrder(request: OrderRequest): Promise<Order> { const order = new Order(request); await this.db.save(order); await this.emailClient.send(order.email, "Order confirmed"); return order; }} // COST: None beyond the code itself// BENEFIT: Simple, obvious// DOWNSIDE: Hard to test without real database and email service // ============================================// THE SERVICE WITH DI// ============================================interface Database { save(entity: any): Promise<void>; }interface EmailClient { send(to: string, body: string): Promise<void>; } class OrderService { constructor( private db: Database, private emailClient: EmailClient ) {} async createOrder(request: OrderRequest): Promise<Order> { const order = new Order(request); await this.db.save(order); await this.emailClient.send(order.email, "Order confirmed"); return order; }} // COST:// - 2 interfaces added// - Constructor signature more complex// - Need a composition root to wire things up// - Need to understand DI conceptually // BENEFIT:// - Testing with mock DB and email is trivial// - Different environments can use different implementations// - Dependencies are explicit and visible // ============================================// VERDICT: DI is justified IF AND ONLY IF:// - You're actually going to test this class in isolation// - OR you need different implementations for different contexts// - AND the team understands DI patterns // If you're not testing, or testing only integration tests,// the simpler version may be better.// ============================================This calculation requires honesty. 'We might need it' isn't a benefit—it's speculation. 'It's the right way' isn't a benefit—it's opinion. Only concrete, demonstrable improvements count on the benefit side.
Having spent significant effort cautioning against pattern overuse, let's establish clear, rigorous criteria for when patterns are genuinely necessary. These aren't suggestions—they're requirements for pattern application.
| Pattern | Genuine Need | Pattern for Pattern's Sake |
|---|---|---|
| Strategy | You have 3+ algorithms and switch between them at runtime | You have 1 algorithm and 'might' have more |
| Factory Method | Subclasses determine which object to create; polymorphism is real | You create one type but want to 'encapsulate creation' |
| Observer | Multiple, varying consumers need updates; you don't know them all | One component tells another; you could just call a method |
| Decorator | You combine behaviors combinatorially at runtime | You add one behavior that never varies |
| Command | You need undo, logging, or queuing of operations | You're invoking a method and nothing more |
| Singleton | Exactly one instance is required by resource constraints | You want global access to a convenient object |
Before applying any pattern, complete this sentence: 'Without this pattern, we cannot [concrete capability].' If you can't complete it with a specific capability that's currently blocked, you don't need the pattern.
Refusing patterns requires confidence—confidence that simplicity is a virtue, that expertise includes restraint, and that solving today's problems beats preparing for tomorrow's imaginations.
Healthy pattern skepticism isn't anti-pattern dogma. It's a calibrated default toward simplicity that requires patterns to prove their worth.
The skeptic's questions:
'What problem does this solve?'
'What's the simple alternative?'
'Who benefits from this pattern?'
'What's the cost of being wrong?'
Pattern pressure often comes in code reviews. Here's how to push back constructively:
Pattern advocate: 'This should use the Strategy pattern for the different validation types.'
Skeptic response: 'Currently we have two validation types that never change. Can you describe a scenario where we'd need to add or swap strategies at runtime? If not, the switch statement is clearer and we can refactor when the need arises.'
Pattern advocate: 'We should add an interface for this class so it can be mocked in tests.'
Skeptic response: 'Are we planning to write unit tests that mock this component? If we're doing integration or end-to-end tests, the interface adds no value. Let's add it when we need it.'
Pattern advocate: 'This should go through a Factory for consistent object creation.'
Skeptic response: 'What does the Factory provide that the constructor doesn't? If it's just calling new with the same parameters, it's an extra layer without benefit.'
These conversations work best when both parties genuinely explore the question. The skeptic might be wrong—the pattern might be necessary. The goal is understanding, not winning. But the default should be simplicity, with patterns requiring justification.
In some organizations, simplicity needs cultural backing. Here's how to build it:
Establish 'Simple First' as a Principle
Celebrate Simplifications
Question Complexity in Architecture Reviews
Measure What Matters
Let's examine several scenarios where pattern skepticism guides better design decisions.
Situation: A team building a small internal tool adds a Repository layer over their database, with interfaces for each repository and multiple implementations.
Justification: 'This allows us to swap databases or use in-memory storage for tests.'
Reality check:
Skeptic's verdict: The Repository layer adds 15 classes with no practical benefit. Direct database access through a simple data access object would suffice. If database portability ever becomes a requirement, the abstraction can be added then—informed by actual requirements rather than speculation.
The simpler alternative:
// Instead of: IUserRepository, UserRepository, InMemoryUserRepository, etc.
// Just use:
class UserData {
constructor(private db: Database) {}
async findById(id: string): Promise<User | null> {
return this.db.query('SELECT * FROM users WHERE id = $1', [id]);
}
// Direct, clear, testable with a real test database
}
Situation: A notification system uses an Observer pattern with Subject and Observer interfaces, concrete implementations, and a registration mechanism.
Justification: 'This decouples the notification sender from receivers.'
Reality check:
Skeptic's verdict: Direct method invocation is simpler and achieves the same result:
// Instead of:
class OrderSubject implements Subject {
notify(event: OrderEvent) { this.observers.forEach(o => o.update(event)); }
}
class EmailObserver implements Observer {
update(event: OrderEvent) { /* send email */ }
}
// Just use:
class OrderService {
async createOrder(request: OrderRequest) {
const order = await this.saveOrder(request);
await this.emailService.sendOrderConfirmation(order);
}
}
The direct call is more readable, easier to debug, and loses nothing because there's no polymorphism in the observers.
Situation: Object creation goes through a Factory class that constructs exactly one type of object.
Justification: 'This centralizes creation logic and allows us to change how objects are created.'
Reality check:
Skeptic's verdict: A constructor is already a factory for its class. Adding another layer is pure ceremony:
// Instead of:
class UserFactory {
create(name: string, email: string): User {
return new User(name, email);
}
}
// Just use:
const user = new User(name, email);
// When creation becomes complex:
class User {
// Put the complexity where it belongs: in the class itself
static createWithDefaults(name: string): User {
return new User(name, `${name}@default.com`, DefaultRole, new Date());
}
}
Factories are for when you don't know which class to instantiate, or when creation requires complex coordination of multiple objects. For straightforward construction, constructors exist for a reason.
In each case, the pattern was applied in anticipation of needs that don't exist. The skeptic's approach is to wait until the need materializes. Adding patterns later—with knowledge of actual requirements—is cheaper than maintaining premature patterns.
Pattern wisdom—knowing when to use patterns and when to refrain—develops through experience, reflection, and deliberate practice. Here's how to cultivate it:
The journey of pattern maturity:
Stage 1: Pattern Ignorance
Stage 2: Pattern Discovery
Stage 3: Pattern Disillusionment
Stage 4: Pattern Wisdom
Most developers who learn patterns go through Stages 1-3. The goal is to reach Stage 4—knowing when not to use the tools you've mastered.
The Gang of Four, who wrote the original design patterns book, have said they would write it differently today—with more emphasis on when patterns shouldn't be used. Even the pattern pioneers recognize the danger of pattern worship.
We've explored the anti-pattern of 'pattern for pattern's sake' in depth. Let's consolidate the key insights:
What's next:
Having explored over-patterning and pattern worship, we'll conclude this module with the positive principle that should guide all design decisions: Keeping It Simple. We'll examine simplicity as a design virtue, how to achieve it while meeting real requirements, and how to balance simplicity with the genuine complexity that some problems require.
You now understand the anti-pattern of 'pattern for pattern's sake'—the application of patterns as an end rather than a means. Armed with this awareness, you can evaluate pattern decisions with appropriate skepticism while remaining open to patterns that genuinely serve your projects.