Code Smells: The 12 Most Common and How to Fix Them
The 12 most common code smells, what they indicate about technical debt and concrete steps to fix each one without breaking production.
In this article:
- What are code smells and why they matter
- Structural code smells (1 to 6)
- Behavioral and process code smells (7 to 12)
- How to fix code smells without breaking production
- Conclusion
What are code smells and why they matter
Code smells are surface-level indicators of deeper problems in a codebase. The term, popularized by Martin Fowler and Kent Beck, refers to characteristics of code that suggest the presence of technical debt, poor design or fragility, even if the code technically works. They are not bugs. The code runs. But it has properties that make it harder to change, test and maintain than it should be.
What are code smells in practical terms? They are the patterns that make engineers say “I do not want to touch this” or “I am not sure what would break if I changed this.” They are the signatures of a codebase that has accumulated decisions that made sense individually but create collective friction at scale.
The 12 code smells described here appear consistently across codebases we assess. Each has a specific signature that can be identified through static analysis or code review, and each has a defined remediation path that does not require a complete rewrite.
Structural code smells (1 to 6)
**1. **God Class. A class with too many responsibilities. Typical signatures: 500+ lines, 30+ methods, dependencies on a large number of other classes. The God Class violates the Single Responsibility Principle and creates a bottleneck where changes of many types converge. Fix: apply the Extract Class refactoring to separate distinct responsibilities into their own classes, starting with methods that do not share state with the rest.
2. Long Method. A function or method that tries to do too much in a single block of code. Functions above 50 lines are difficult to test and understand. Functions above 100 lines are almost always a sign that multiple responsibilities have been conflated. Fix: Extract Method refactoring, breaking the long method into smaller, named sub-functions that describe what each step does.
3. Feature Envy. A method that spends more time accessing data from another class than from its own. This indicates that the method is in the wrong class. Fix: move the method to the class whose data it relies on most.
**4. **Spaghetti code. Code where control flow jumps between functions, event handlers and modules in ways that cannot be traced linearly. This pattern emerges from iterative feature addition without architectural review. Fix: map the actual flow, identify entry and exit points and introduce a clear orchestration layer that owns the flow logic.
5. Data Clumps. Groups of data that always appear together, such as startDate, endDate and timezone passed as three separate parameters to five different methods. The fix is to create a value object that encapsulates them.
6. Primitive Obsession. Using primitive types (strings, integers) to represent domain concepts. A phone number stored as a string with no validation is a primitive obsession. The fix is to introduce value objects with built-in validation and behavior.
Behavioral and process code smells (7 to 12)
7. Shotgun Surgery. A single change requires modifications to many different classes. This indicates that a concern is spread across the codebase rather than encapsulated. Every time a business rule changes, it needs to be applied in ten places. Fix: consolidate the scattered concern into a single location.
8. Divergent Change. A single class is modified for many different reasons. The authentication module gets changed when the login UI changes, when the session management changes, when the audit logging requirements change and when the password policy changes. These are four different concerns living in the same place. Fix: extract each concern into its own module.
**9. **Dead code. Code that is never executed: unused variables, unreachable branches, deprecated methods left for safety. Dead code creates cognitive overhead, inflates the codebase and can contain security vulnerabilities. Fix: identify via static analysis or coverage tooling and delete. The version control system preserves the history.
10. Speculative Generality. Code written to handle cases that do not yet exist, “just in case.” Unused hooks, abstract classes with a single implementation, configuration options that no one ever sets. This is the technical debt of anticipation. Fix: remove the abstraction and simplify until there is a real requirement for it.
11. Message Chains. Code that calls a method, gets an object, calls another method, gets another object, and so on: user.getAddress().getCity().getCountry().getCode(). This creates tight coupling to the internal structure of each intermediate object. Fix: apply the Law of Demeter and introduce intermediary methods that encapsulate the chain.
12. Inappropriate Intimacy. Two classes that access each other’s private members or are tightly aware of each other’s internal implementations. This is a coupling smell that makes it impossible to change one class without touching the other. Fix: introduce a clear interface or boundary between the two classes.
How to fix code smells without breaking production
The risk in addressing code smells in a live codebase is that refactoring changes code behavior in ways that tests do not catch. The safest approach follows a consistent sequence.
First, ensure test coverage of the code you are about to change. If coverage is absent, write characterization tests: tests that document the current behavior, even if that behavior is wrong. These tests ensure that the refactoring does not accidentally change behavior.
Second, apply the smallest possible refactoring step. Extract one method, one class or one value object at a time. Run the tests after each step. If they pass, commit and continue. If they fail, revert and investigate.
Third, verify that the relevant code health metrics improved after the change. If you extracted a God Class and the cyclomatic complexity of the original class did not decrease, the extraction did not achieve its purpose.
Our legacy modernization service applies this incremental approach to codebases with years of accumulated smells, producing measurable improvements without production downtime.
Conclusion
Code smells are the surface manifestation of technical debt. They do not always indicate a crisis, but they are reliable early warning signs. The 12 described here cover the patterns we find in almost every codebase we assess.
Addressing them requires the same discipline as managing any other technical investment: identify, prioritize by business impact, apply incremental fixes with test coverage and verify the result against measurable metrics.
Eden Technologies has helped over 200 engineering teams move from codebases where “no one wants to touch anything” to systems where changes are confident, deployments are predictable and incidents are rare.
Does your codebase have these problems? Let’s talk about your system