Pattern Tradeoffs and Anti-Patterns
Design patterns are tools.
A good tool solves a specific problem.
The wrong tool can make simple work harder.
This lesson reviews how to choose patterns wisely and avoid common anti-patterns.
Start With the Problem
Before choosing a pattern, describe the problem in plain language.
Good problem statements:
- Object creation is duplicated in five places.
- API response shapes are leaking into UI code.
- A button should trigger different actions depending on configuration.
- Several services need to react when the current user changes.
- Tests are hard because functions import real browser storage directly.
Weak problem statements:
- We should use more patterns.
- This code does not look enterprise enough.
- We may need every possible variation someday.
Patterns should answer a real design pressure.
Match Problems to Patterns
| Problem | Possible pattern |
|---|---|
| Repeated object creation | Factory |
| One shared infrastructure service | Singleton, with caution |
| Many listeners reacting to change | Observer or pub/sub |
| Interchangeable algorithms | Strategy |
| External interface does not match your app | Adapter |
| Complex workflow needs a simple API | Facade |
| Add behavior around a function | Decorator or wrapper |
| Queue, retry, log, or undo actions | Command |
| Traverse values consistently | Iterator |
| Replace services in tests | Dependency injection |
| Combine flexible capabilities | Composition |
This table is a starting point, not a rulebook.
Prefer the Smallest Useful Pattern
A pattern does not need a class hierarchy.
This strategy is enough for many cases:
const validators = {
email(value) {
return value.includes("@");
},
required(value) {
return value.trim() !== "";
},
};This factory is enough for many cases:
function createUser(name) {
return {
name,
active: true,
};
}Use the smallest version that makes the design clearer.
Anti-Pattern: God Object
A god object owns too many responsibilities.
const system = {
users: [],
products: [],
cart: [],
renderPage() {},
saveUser() {},
calculateTax() {},
sendEmail() {},
trackAnalytics() {},
};This object becomes hard to test and risky to change.
Better: split responsibilities by purpose.
const userService = createUserService();
const cartService = createCartService();
const analytics = createAnalyticsClient();Anti-Pattern: Hidden Global State
Hidden global state appears when many parts of the app read and write shared values.
export const appState = {
currentUser: null,
cart: [],
theme: "light",
};This may start convenient, but changes become hard to trace.
Prefer explicit functions, scoped state, or well-defined state management.
Anti-Pattern: Pattern Soup
Pattern soup happens when several patterns are stacked without improving clarity.
Example warning signs:
- many files exist for one small behavior
- every action passes through several managers
- pattern names appear more often than domain names
- debugging requires jumping across many layers
A simple function may be better.
Anti-Pattern: Premature Abstraction
Premature abstraction means creating a flexible design before you know what needs to vary.
function createUniversalInputStrategyFactory(config) {
// built for many cases that do not exist yet
}It is often better to write clear direct code first.
When real duplication or variation appears, extract the pattern then.
Anti-Pattern: Leaky Abstraction
An abstraction leaks when callers still need to understand hidden details.
const user = adaptUserFromApi(apiUser);
// Later in UI code:
console.log(user.user_id); // API field leaked into app codeIf your app model uses id, keep the external user_id field inside the adapter.
Balancing Readability and Flexibility
Readable code is not always the shortest code.
Flexible code is not always the best code.
A good design balances:
- what the code needs today
- how likely the behavior is to change
- how easy it is to test
- how easy it is for another developer to follow
- how consistent it is with the rest of the project
Refactoring Into Patterns
You do not need to design everything perfectly at the start.
Many patterns are easiest to introduce during refactoring.
Example path:
- Start with direct code.
- Notice repeated creation or branching.
- Extract a factory or strategy.
- Add tests around the new boundary.
- Keep the public interface small.
This approach is often safer than guessing early.
Practical Checklist
Before adding a pattern, ask:
- Does this pattern remove real duplication or coupling?
- Does it make the calling code easier to read?
- Are the names based on the app's domain, not only the pattern?
- Can the pattern be tested independently?
- Can a simpler function or module solve the problem?
- Does it match the surrounding code style?
If the pattern fails these questions, simplify.
Common Mistakes
Optimizing for Imaginary Requirements
Design for likely change, not every possible change.
Copying Examples Too Literally
Classic pattern examples may use classes because they came from other languages.
In JavaScript, a function or module may be clearer.
Ignoring Team Understanding
Code is read more than it is written.
A clever pattern is not helpful if the team cannot maintain it.
Best Practices
- Name code after business concepts first, pattern names second.
- Keep interfaces small and consistent.
- Prefer simple functions until a pattern solves a visible problem.
- Add patterns through refactoring when possible.
- Test behavior at pattern boundaries.
- Remove abstractions that no longer pay for themselves.
Summary
Patterns help when they make real code easier to understand, test, and change.
They hurt when they add ceremony, hidden state, or imaginary flexibility.
The goal is not to use every pattern.
The goal is to choose the simplest design that handles the problem clearly.