Design anti-patterns that cost more than bugs 🚨
Bugs cause damage, but design anti-patterns have a special talent: they do damage slowly, plausibly, and often while pretending to bring more order. The real problem is that they raise the cost of change until every intervention becomes delicate surgery.
This guide is not for collecting labels. It is for recognizing structural smells before they become permanent furniture in the codebase.
God Object: everything goes through it 👑
The God Object concentrates too many responsibilities in a single component. It knows everything, decides everything, coordinates everything, and would probably also like opinions on your personal roadmap.
Typical signs:
- huge and unmanageable files
- dependencies everywhere
- slow and fragile tests
- widespread fear of touching that component
Typical fix:
- separate responsibilities by reason to change
- extract cohesive services or modules
- reduce unnecessary dependencies at the edges
Spaghetti Code: flow without geometry 🍝
Spaghetti Code is not just ugly code. It is code where the flow is hard to follow, dependencies are unreadable, and changing one behavior triggers side effects you only discover later.
Typical signs:
- deeply nested conditionals
- logic spread across distant places
- state mutated by too many actors
- debugging that feels like a city chase with no map
Typical fix:
- clarify flow with smaller functions and sharper responsibilities
- reduce shared state and implicit coupling
- isolate I/O, orchestration, and domain logic
Premature Abstraction: abstracting too early is expensive 🏗️
Premature Abstraction usually starts from a respectable intention: avoid duplication and prepare for the future. The result is often a generic layer that does not fit any real case particularly well.
Typical signs:
- vague names like
BaseManager,GenericService,AbstractProcessor - flags and parameters that alter behavior in too many ways
- code that is harder to understand than the concrete cases it supposedly unifies
Typical fix:
- move design closer to real use cases
- accept temporary duplication when it clarifies the domain
- abstract only after a stable pattern appears
Shotgun Surgery: one change, twenty files 💥
Shotgun Surgery happens when a single change forces you to touch many different points in the codebase.
Typical signs:
- even simple features require a long list of micro-edits
- related logic is scattered across too many modules
- high risk of forgetting one required change
Typical fix:
- bring together data and behavior that change together
- increase cohesion
- make responsibility boundaries more explicit
Golden Hammer: one pattern to rule them all 🔨
Golden Hammer is the habit of applying the same solution regardless of the problem. It happens with microservices, DDD, workflow engines, event buses, and yes, also with dependency injection.
Typical signs:
- the solution arrives before the analysis
- every problem starts looking like the team’s favorite tool
- the concrete context matters less than the current technical fashion
Typical fix:
- go back to the problem and its constraints
- compare several options with explicit trade-offs
- avoid performance architecture for imaginary requirements
Two very common modern anti-patterns 🧪
Premature interfaces
Creating interfaces everywhere “for flexibility” often produces boilerplate and artificial contracts. Interfaces are useful when there is real variability to isolate, not as preventive decoration.
DI everywhere
Dependency injection is useful, but putting a DI layer around every struct turns a normal codebase into a fashion show of constructors, mocks, and unnecessary wiring. If there is no clear reason to swap a dependency, the concrete dependency can remain concrete.
How to use this guide in reviews 🔍
Useful questions:
- is this component doing too much?
- did this abstraction emerge from a real case or from anticipatory anxiety?
- does a local change remain local?
- is the team applying a pattern because it helps or because it is familiar?
In short 🧾
Design anti-patterns are not moral sins. They are signals that a system is becoming more expensive to change, understand, and test. Recognizing them early is worth more than many dramatic retrospectives after the damage has already hardened into structure.
For the principles that help prevent them, see software design principles, Clean code, and Dependency Injection and Inversion of Control.