Common Code Smells and How to Fix Them

Naveen AnamaneniUpdated · 7 min read

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

SmellHow to spot itUsual fix
Long methodYou scroll to read it; it has comment headers insideExtract Method
Large class (god class)Hundreds of lines, many unrelated fieldsSplit by responsibility (Extract Class)
Duplicate codeThe same few lines appear in several placesExtract one method and call it
Long parameter listFour or more parameters, often passed togetherIntroduce a record (parameter object)
Magic numbers and stringsif (fee > 14500) with no explanationNamed constants or an enum
Primitive obsessionPhone numbers, money and grades as plain String or intSmall types: record Money(long paise)
Switch on typeThe same switch on a type code appears in many placesPolymorphism, or a sealed interface with pattern matching
Feature envyA method uses another object's data more than its ownMove the method to that class
Data clumpsThe same three fields always travel togetherGroup them in a record
Dead codeMethods and variables nobody uses; commented-out codeDelete it; Git keeps history
Speculative generalityInterfaces and options "for the future" that nobody usesRemove them until they are needed
Comments explaining confusing codeA long comment above a tangled blockRename 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.

Fees.java▶ Try it
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);
    }
}
Output
Navika: 13950
Advitha: 14400

The fix is to name the numbers and write the rule once.

Fees.java▶ Try it
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));
    }
}
Output
Navika: 13950
Advitha: 14400

Explanation: 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.

Transport.java▶ Try it
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));
    }
}
Output
1040
1500
0

A sealed interface with a record per type lets the compiler check that every type is handled, and keeps each type's data with it.

Transport.java▶ Try 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()));
    }
}
Output
1040
1500
0

Explanation:

  1. A typo such as "BSU" compiled fine in the first version and failed at runtime. Now new Bsu(12) doesn't compile.
  2. The switch has no default. Because Mode is sealed, the compiler knows the three cases are complete. If someone adds record Cycle() implements Mode, every switch that forgot it becomes a compile error, not a hidden bug.
  3. Only Bus carries km, 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

  1. 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).
  2. 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 the switch.

Quick check: Code smells

4 questions
1What is a code smell?
2What is the usual fix for magic numbers?
3Why is a sealed interface better than a switch on strings?
4Which smell is a method that mostly uses another class's data?

Key Takeaways

Was this tutorial helpful?

Lesson 538 of 578