20 Code Smells Every Developer Should Know
"The code compiles."
That's often where the real problems begin.
Many software failures don't originate from complex algorithms or cutting-edge technologies. They stem from small design decisions that quietly accumulate over months or years until the codebase becomes difficult to understand, risky to modify, and expensive to maintain.
These warning signs are known as code smells.
A code smell isn't necessarily a bug. Instead, it's an indicator that something in the design could be improved. Left unaddressed, code smells often evolve into technical debt, slower development, more defects, and frustrated engineering teams.
After more than three decades building enterprise software, I've learned that experienced developers don't simply write code that works—they write code that remains understandable six months later.
Here are 20 code smells every developer should recognize.
1. Long Method
Smell
A method grows to hundreds of lines and performs multiple responsibilities.
Why it's dangerous
Difficult to understand
Hard to test
Bugs hide easily
Impossible to reuse
Better approach
Break large methods into smaller functions, each with one clear responsibility.
2. Large Class
Smell
A class keeps growing until it handles business logic, validation, persistence, logging, notifications, and reporting.
Why it's dangerous
Violates the Single Responsibility Principle.
Better approach
Split responsibilities into focused services or domain objects.
3. Duplicate Code
Smell
The same logic appears in multiple places.
calculateDiscount()
calculateDiscountForVIP()
calculateDiscountForMember()
Nearly identical.
Why it's dangerous
Every bug fix must be repeated.
Soon the copies begin to diverge.
Better approach
Extract common behavior into reusable components.
4. God Object
Smell
One class knows everything and controls everything.
Example:
UserService
contains:
authentication
billing
email
reporting
inventory
notifications
Better approach
Divide responsibilities into cohesive services.
5. Long Parameter List
createOrder(
customer,
items,
payment,
shipping,
coupon,
currency,
tax,
warehouse,
locale,
source,
metadata)
Nobody remembers parameter order.
Better approach
Use Parameter Objects or Builder Pattern.
6. Primitive Obsession
Instead of creating domain objects:
String email
String phone
String postcode
used everywhere.
Better approach
Create meaningful value objects.
Email
PhoneNumber
Address
Money
7. Feature Envy
A method spends most of its time manipulating another object's data.
OrderService
knows more about
Customer
than Customer itself.
Better approach
Move behavior closer to the data it belongs to.
8. Data Clumps
The same variables always appear together.
street
city
postcode
country
Better approach
Create an Address object.
9. Switch Statements Everywhere
Large switch statements continue growing.
switch(type){
...
}
Better approach
Use polymorphism or the Strategy Pattern.
10. Shotgun Surgery
One business change requires edits across dozens of files.
Why it's dangerous
High risk of missing something.
Better approach
Improve modularity and encapsulation.
11. Divergent Change
One class changes for many unrelated reasons.
Today:
- pricing
Tomorrow:
- logging
Next week:
- reporting
This indicates poor separation of concerns.
12. Comments Explain Bad Code
If comments constantly explain what the code does, the code probably isn't expressive enough.
Instead of:
// calculate tax
Write:
calculateTax()
Good naming beats excessive comments.
13. Magic Numbers
if(score > 87)
Why 87?
Nobody knows.
Better approach
PASSING_SCORE
14. Inappropriate Intimacy
Two classes know too much about each other's internals.
Changing one breaks the other.
Better approach
Reduce coupling through proper interfaces.
15. Message Chains
a.getB()
.getC()
.getD()
.getE()
Also known as the "train wreck."
Better approach
Follow the Law of Demeter.
16. Lazy Class
A class exists but barely does anything.
Sometimes:
UserManagerFactoryHelperUtil
contains only one method.
Delete it.
17. Temporary Fields
Fields only exist during one specific workflow.
The rest of the time they remain unused.
This usually suggests missing abstraction.
18. Dead Code
Unused methods.
Unused variables.
Unused classes.
Unused APIs.
They're not harmless.
They increase cognitive load and confuse future developers.
Delete them.
19. Speculative Generality
Building abstractions for requirements that may never exist.
"We might support five databases someday."
Three years later...
Still one database.
Keep designs as simple as current requirements allow.
20. Exception Swallowing
try{
...
}catch(Exception e){
}
Nothing logged.
Nothing reported.
Failures disappear silently.
This makes production debugging a nightmare.
Always handle exceptions intentionally.
How Senior Engineers Spot Code Smells
Experienced developers don't just review syntax.
They ask questions like:
Is this class doing too much?
Can this code be understood in one minute?
Will future developers know why this exists?
How expensive will the next change be?
Does this design communicate intent?
Great code isn't measured by cleverness.
It's measured by clarity, maintainability, and ease of change.
Final Thoughts
Every developer writes code smells from time to time. The difference is that great engineers learn to recognize them early and refactor continuously instead of allowing them to accumulate.
Code reviews shouldn't focus only on formatting or naming—they should identify structural issues before they become technical debt.
The cleanest code isn't the shortest or the most sophisticated.
It's the code that another developer can understand, modify, and trust with confidence months or even years later.
Which code smell do you encounter most often in your projects? Share your experience in the comments—I’d love to hear how your team keeps codebases clean and maintainable.
