What does 'code smell' mean?
Kent Beck coined the term in the late 1990s, and Martin Fowler made it widely known in Refactoring (1999), which pairs each smell with refactorings that remove it. The metaphor is deliberate: a smell is a hint to look closer, not proof that something is wrong. A long function may be perfectly fine; the smell is the prompt to ask whether it should be split.
Smells matter because software spends most of its life being changed. Code that is hard to read takes longer to change safely, attracts copy-paste fixes, and hides bugs from reviewers. Removing smells in small steps keeps that cost from compounding.
Types of code smells: the five categories
Fowler's original list has been grouped into five categories, following a taxonomy proposed by Mäntylä and colleagues in 2003. The categories make smells easier to recognise because each group has a shared cause.
| Category | What goes wrong | Smells in this group |
|---|---|---|
| Bloaters | Code grows too large to work with | Long Method, Large Class, Primitive Obsession, Long Parameter List, Data Clumps |
| Object-orientation abusers | OO features used incompletely or wrongly | Switch Statements, Temporary Field, Refused Bequest, Alternative Classes with Different Interfaces |
| Change preventers | One change forces many others | Divergent Change, Shotgun Surgery, Parallel Inheritance Hierarchies |
| Dispensables | Something that isn't needed | Duplicate Code, Dead Code, Lazy Class, Data Class, Speculative Generality, Comments that explain unclear code |
| Couplers | Classes too tightly linked | Feature Envy, Inappropriate Intimacy, Message Chains, Middle Man |
List of code smells with examples and fixes
The most common smells, what they look like, and the refactoring that usually removes them:
- Long Method. A function that does several things. Fix: Extract Function for each step, named after what it does.
- Large Class (God Object). A class with too many fields and responsibilities. Fix: Extract Class along its responsibilities.
- Primitive Obsession. Strings and numbers used for domain concepts such as money, emails or IDs. Fix: small value types that validate themselves.
- Long Parameter List. Many arguments, easily passed in the wrong order. Fix: Introduce Parameter Object.
- Data Clumps. The same values always travel together (start, end, timezone). Fix: make them one type.
- Switch Statements. The same switch on a type repeated in several places. Fix: polymorphism or a lookup table.
- Temporary Field. A field that is only set in some situations and empty otherwise. Fix: move it into its own object.
- Refused Bequest. A subclass that ignores most of what it inherits. Fix: replace inheritance with composition.
- Divergent Change. One module changes for many unrelated reasons. Fix: split it so each part has one reason to change.
- Shotgun Surgery. One change requires edits in many files. Fix: Move Function / Move Field to gather the logic.
- Duplicate Code. The same logic in several places, drifting apart. Fix: extract a shared function — once the copies are truly the same concept.
- Dead Code. Functions, flags and branches nothing uses. Fix: delete them; version control remembers.
- Lazy Class. A class that does too little to justify existing. Fix: inline it.
- Data Class. A class with fields and no behaviour, while other code manipulates its data. Fix: move that behaviour into the class.
- Speculative Generality. Abstractions built for needs that never arrived. Fix: inline them until a second real use appears.
- Feature Envy. A method that uses another object's data more than its own. Fix: move the method to that data.
- Inappropriate Intimacy. Classes reaching into each other's internals. Fix: move methods and fields, or hide internals behind an interface.
- Message Chains. Calls like order.customer().address().city(). Fix: Hide Delegate — ask for what you need.
- Middle Man. A class that only forwards calls. Fix: remove it and call the real object.
Example: Primitive Obsession with money
Representing money as a floating-point number is a classic smell that turns into real bugs: rounding errors and mixed currencies.
function applyDiscount(price: number, percent: number) {
return price - price * (percent / 100); // rounding drift on some amounts
}
// Better: integer minor units with an explicit currency
type Money = { amountMinor: number; currency: "INR" | "USD" };
function applyDiscount(price: Money, percent: number): Money {
const off = Math.round((price.amountMinor * percent) / 100);
return { ...price, amountMinor: price.amountMinor - off };
}Python code smells
Every language has smells that come from its own features. Python's most common ones include mutable default arguments, bare except clauses that swallow every error, wildcard imports that hide where names come from, deeply nested conditionals, and 'god modules' that collect unrelated helpers.
# Smell: mutable default argument — the list is shared between calls
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
def add_tag(tag, tags=None):
tags = [] if tags is None else tags
tags.append(tag)
return tags
# Smell: bare except hides every failure, including KeyboardInterrupt
try:
charge(order)
except:
pass
try:
charge(order)
except PaymentDeclined as e:
log.warning("payment declined", extra={"order_id": order.id, "reason": e.code})Code smells in SonarQube
SonarQube has long used 'code smell' as its name for maintainability issues — alongside bugs (reliability) and vulnerabilities (security). Its rules flag measurable smells such as overly complex functions (cognitive complexity), duplicated blocks, long parameter lists, unused variables and commented-out code, and its technical-debt estimate is largely built from them. Newer versions group findings by software quality — maintainability, reliability and security — but the idea is the same.
Rule-based tools are good at smells that can be measured. Design smells such as Feature Envy or Shotgun Surgery depend on what the code means, so they are usually found in code review.
Code smells vs bugs vs anti-patterns
A bug is code that produces the wrong result. A code smell is code that works today but is hard to understand or change safely. An anti-pattern is a common, named solution that tends to cause more problems than it solves; some smells, such as the God Object, are also anti-patterns.
The distinction matters in review: a bug blocks the merge, while a smell is usually a suggestion — unless the change makes an existing smell significantly worse.
How to find code smells
- Linters and static analysis. Measurable smells: function length, complexity, duplication, unused code (ESLint, Pylint, SonarQube, PMD).
- Code review. Design smells such as Feature Envy and Shotgun Surgery, which need an understanding of the domain.
- AI code review. Smells in context — for example a new helper that duplicates one that already exists elsewhere in the repository — with the suggested refactoring.
- Change history. Files that change in almost every pull request, or always change together, often hide Shotgun Surgery or Divergent Change.
When to fix a code smell — and when not to
Fix smells in code you are already changing — the 'leave it a little cleaner' rule — and only when tests cover the behaviour. Avoid large refactors mixed into feature pull requests: they make review harder and hide behaviour changes. If a smell needs a big change, do it in its own pull request.
Leave smells in stable code nobody touches. A smell only costs something when someone has to read or change the code.
Code smell review checklist
- New functions do one thing and fit on a screen.
- The change doesn't copy logic that already exists elsewhere.
- Functions take few parameters; related values travel as one type.
- Domain values such as money, IDs and emails have their own types.
- No new switch-on-type chains duplicating existing ones.
- Methods live with the data they use.
- Unused code, flags and parameters introduced by the change are removed.
- Refactoring is in a separate pull request from behaviour changes.
How Mesrai helps with code smells
Mesrai flags maintainability problems in the context of your repository — such as a new helper that duplicates one that already exists, or money handled as a float — and suggests the refactoring. Severity filters keep smells as low-priority suggestions so they never drown out real bugs.
- Smells flagged with the suggested refactoring
- Repository graph spots duplication across files
- Severity filters keep suggestions separate from blocking issues
- Your own style rules as Mesrai Rules