Common Code Smells and How to Fix Them
When milk starts to go off, you smell it before you see anything wrong. The milk is still liquid, still white, and you could still pour it. The smell is a warning that trouble is close. Code has the same kind of warning signs. A program can compile, pass its tests and run correctly, and still have parts that make every future change slow and risky. These warning signs are called code smells. In this lesson, you will learn what a code smell is, the smells you will meet most often in Java, and the usual fix for each.
What Is a Code Smell?
A code smell in Java is a pattern in working code that is used as a hint that the design could be better. It isn't a bug: the program does what it should. It is a sign that the code will be hard to read, test or change later. The term was made popular by Martin Fowler and Kent Beck in the book Refactoring.
Note that a smell is a hint, not a rule. Sometimes a long method is the clearest way to write something. The skill is to notice the smell and then decide whether it is worth fixing.
The Most Common Smells
| Smell | How to spot it | Usual fix |
|---|---|---|
| Long method | You scroll to read it; it has comment headers inside | Extract Method |
| Large class (god class) | Hundreds of lines, many unrelated fields | Split by responsibility (Extract Class) |
| Duplicate code | The same few lines appear in several places | Extract one method and call it |
| Long parameter list | Four or more parameters, often passed together | Introduce a record (parameter object) |
| Magic numbers and strings | if (fee > 14500) with no explanation | Named constants or an enum |
| Primitive obsession | Phone numbers, money and grades as plain String or int | Small types: record Money(long paise) |
| Switch on type | The same switch on a type code appears in many places | Polymorphism, or a sealed interface with pattern matching |
| Feature envy | A method uses another object's data more than its own | Move the method to that class |
| Data clumps | The same three fields always travel together | Group them in a record |
| Dead code | Methods and variables nobody uses; commented-out code | Delete it; Git keeps history |
| Speculative generality | Interfaces and options "for the future" that nobody uses | Remove them until they are needed |
| Comments explaining confusing code | A long comment above a tangled block | Rename and extract until the comment isn't needed |
Smell: Magic Numbers and Duplicate Code
Let's see a small fee program with two smells: the late fine and the sibling discount appear as bare numbers, and the same calculation is written twice.
public class Fees {
public static void main(String[] args) {
boolean navikaLate = true;
int navika = 15000;
if (navikaLate) navika = navika + 500; // paid late
navika = navika - navika * 10 / 100; // has a sibling
System.out.println("Navika: " + navika);
boolean advithaLate = false;
int advitha = 16000;
if (advithaLate) advitha = advitha + 500;
advitha = advitha - advitha * 10 / 100;
System.out.println("Advitha: " + advitha);
}
}Navika: 13950
Advitha: 14400The fix is to name the numbers and write the rule once.
public class Fees {
private static final int LATE_FINE = 500;
private static final int SIBLING_DISCOUNT_PERCENT = 10;
static int finalFee(int baseFee, boolean late, boolean hasSibling) {
int fee = late ? baseFee + LATE_FINE : baseFee;
if (hasSibling) {
fee -= fee * SIBLING_DISCOUNT_PERCENT / 100;
}
return fee;
}
public static void main(String[] args) {
System.out.println("Navika: " + finalFee(15000, true, true));
System.out.println("Advitha: " + finalFee(16000, false, true));
}
}Navika: 13950
Advitha: 14400Explanation: the constants LATE_FINE and SIBLING_DISCOUNT_PERCENT explain themselves, and changing the fine now means editing one line instead of hunting for every 500. The rule lives in one method, so it can't drift apart in two places. The output is unchanged.
Smell: Switch on Type
When the same switch on a type code appears in many places, adding a new type means finding and changing every one of them. Let's see an example with fee types written as strings.
public class Transport {
static int monthlyFee(String type, int km) {
switch (type) {
case "BUS": return 800 + km * 20;
case "VAN": return 1500;
case "WALK": return 0;
default: throw new IllegalArgumentException(type);
}
}
public static void main(String[] args) {
System.out.println(monthlyFee("BUS", 12));
System.out.println(monthlyFee("VAN", 5));
System.out.println(monthlyFee("WALK", 1));
}
}1040
1500
0A sealed interface with a record per type lets the compiler check that every type is handled, and keeps each type's data with it.
public class Transport {
sealed interface Mode permits Bus, Van, Walk {}
record Bus(int km) implements Mode {}
record Van() implements Mode {}
record Walk() implements Mode {}
static int monthlyFee(Mode mode) {
return switch (mode) {
case Bus bus -> 800 + bus.km() * 20;
case Van van -> 1500;
case Walk walk -> 0;
};
}
public static void main(String[] args) {
System.out.println(monthlyFee(new Bus(12)));
System.out.println(monthlyFee(new Van()));
System.out.println(monthlyFee(new Walk()));
}
}1040
1500
0Explanation:
- A typo such as
"BSU"compiled fine in the first version and failed at runtime. Nownew Bsu(12)doesn't compile. - The
switchhas nodefault. BecauseModeis sealed, the compiler knows the three cases are complete. If someone addsrecord Cycle() implements Mode, every switch that forgot it becomes a compile error, not a hidden bug. - Only
Buscarrieskm, so you can no longer pass a distance that means nothing for a van.
How to Find Smells
- Code review: a teammate's "I had to read this three times" is a smell report.
- Your own pain: if a small change made you edit five files, look for duplicate code or switch on type.
- Tools: IntelliJ IDEA inspections, SonarLint and PMD flag many smells automatically, such as long methods, duplicate blocks, unused code and magic numbers.
In Practice
Fix smells with refactoring: small steps that change structure but not behavior, with tests run after every step. The next lessons, "Null Handling Strategies in Java" and "Refactoring Basics in Java", cover the safe way to do it.
Not every smell needs fixing right away. Prioritize code that changes often. A smelly class nobody has touched in three years can wait; a smelly class edited every week is costing the team time now.
Common Mistakes
Treating smells as rules. A 40-line method that reads clearly is fine. Use judgment.
Fixing smells without tests. A "cleanup" that changes behavior is a new bug.
Over-correcting. Replacing one magic number with an elaborate configuration system creates speculative generality, another smell.
Mixing cleanup with new features in one commit. Reviewers can't tell which change caused a problem.
Practice
- Easy: List every smell you can find in this method header and fix the ones you can:
static int calc(String t, int a, int b, int c, int d, boolean f). - Harder: Add a fourth transport mode
Cycle(fee 0) to the sealed version. First add only the record and compile, and read the error. Then fix theswitch.
Key Takeaways
Was this tutorial helpful?