Code Smells: What They Are and How They Connect to Technical Debt

Code Smells: What They Are and How They Connect to Technical Debt

Code smells are not bugs. A buggy program crashes, returns wrong results, or fails a test. A program with code smells may run perfectly well for years, and yet every change to it costs more than it should, every new feature carries unexpected risk, and every attempt at refactoring reveals dependencies nobody knew existed. Code smells are the structural characteristics of code that predict future problems: they do not cause an immediate failure, but they make every future change harder, slower, and more dangerous than it needs to be.

The term was popularized by Martin Fowler and Kent Beck in Fowler’s Refactoring: Improving the Design of Existing Code (1999), which catalogued 22 named code smells and matched each to a corresponding refactoring technique. That catalog remains the canonical reference, and the smells Fowler named, Long Method, God Class, Duplicated Code, Feature Envy, Divergent Change, Shotgun Surgery, and others, appear in SonarQube rule sets, static analysis tools, and code review checklists across the industry today.

Clean Up Code Smells

SMART TS XL helps map and fix them across complex systems.

More Info

What Is a Code Smell?

A code smell is a surface characteristic of source code that suggests a deeper structural or design problem. The code compiles, passes tests, and produces correct output, but something about its structure makes it harder to read, extend, or safely modify than it should be. Fowler’s definition: “a surface indication that usually corresponds to a deeper problem in the system.”

Code smells are not violations in the same sense as a syntax error or a failed assertion. They are indicators, patterns that experienced developers recognize as warning signs, even when no immediate failure is visible. The danger is that they are cumulative: a single long method in a 10,000-line codebase is a minor inconvenience. Hundreds of long methods, duplicated logic spread across dozens of modules, and God Classes at the center of the dependency graph is a system that has become genuinely difficult to change safely.

Code Smells vs. Bugs vs. Technical Debt

These three concepts are related but distinct, and confusing them leads to poor prioritization:

ConceptDefinitionImmediate Failure?How to Find
BugCode that produces incorrect behaviorYes, tests fail, users report errorsTesting, monitoring, error logs
Code smellStructural pattern that predicts future problemsNo, code runs correctlyCode review, static analysis
Technical debtThe accumulated cost of past shortcuts and poor decisionsNo, but compounds over timeMetrics, complexity analysis, refactoring effort estimates

Code smells are the mechanism through which technical debt accumulates. Each Long Method added to the codebase is a unit of technical debt incurred; its interest payment is the extra time every future developer spends understanding it, and every future change spends avoiding the side effects of its size.

What Is a Code Smell in SonarQube?

SonarQube classifies code issues into three categories: bugs (definitely wrong), vulnerabilities (security issues), and code smells (maintainability issues). SonarQube’s code smells map directly to Fowler’s catalog and include rules for long methods (above configurable line thresholds), duplicated blocks, too many parameters, complex cognitive complexity scores, missing error handling, and architectural coupling violations. The code smell rules in SonarQube are the industry’s most widely used automated operationalization of Fowler’s original taxonomy.

Martin Fowler’s Code Smells: The Classic Taxonomy

Fowler’s original 22 code smells, organized by category, remain the standard reference. Every major static analysis tool’s ruleset is derived from this taxonomy.

CategoryCode Smells
Bloaters, code that has grown to unwieldy sizeLong Method, Large Class, Primitive Obsession, Long Parameter List, Data Clumps
Object-Orientation Abusers, misuse of OO principlesSwitch Statements, Temporary Field, Refused Bequest, Alternative Classes with Different Interfaces
Change Preventers, make change difficultDivergent Change, Shotgun Surgery, Parallel Inheritance Hierarchies
Dispensables, unnecessary codeComments (excessive), Duplicate Code, Lazy Class, Data Class, Dead Code, Speculative Generality
Couplers, excessive couplingFeature Envy, Inappropriate Intimacy, Message Chains, Middle Man

Understanding which category a smell belongs to helps prioritize remediation: Bloaters and Change Preventers directly correlate with high refactoring cost; Couplers directly correlate with architectural brittleness; Dispensables are the safest to remove.

The Most Common Code Smells: Quick Reference

Code SmellWhat It Looks LikePrimary Risk
Duplicated CodeSame logic appears in multiple placesBug fixes must be applied everywhere; copies diverge over time
Long MethodMethods exceeding 20-30 lines with multiple responsibilitiesHigh cognitive load; hard to test isolated behavior
God Class / Large ClassOne class that does everythingEvery feature change touches the same class; merge conflicts, fragility
Long Parameter ListMethods taking 4+ parametersEasy to pass wrong values; hard to read call sites
Feature EnvyA method that uses another class’s data more than its ownTight coupling; change in one class breaks the other
Divergent ChangeOne class modified for many different reasonsViolates single responsibility; unpredictable side effects
Shotgun SurgeryOne change requires edits across many classesHigh change cost; easy to miss an instance
Dead CodeCode that is never called or reachedConfuses developers; accumulates over years; complicates migration
Primitive ObsessionUsing basic types (strings, ints) instead of domain objectsValidation scattered everywhere; poor expressiveness
Data ClumpsSame group of fields passed together repeatedlyShould be a domain object; signals missing abstraction
Speculative GeneralityCode written for imagined future needsUnnecessary complexity; nobody understands why it’s there
Inconsistent Error HandlingSilent catches, varying exception strategiesFailures go undetected; debugging takes much longer

Code Smell Definitions and Examples

Duplicated Code

The most common and most expensive code smell in large systems. Duplication arises from copy-paste development, from time pressure, and from teams working in silos who independently solve the same problem. The immediate consequence is a maintenance tax: every change to shared logic must be applied to every copy.

java

// ServiceA -- discount calculation
double calculateDiscount(double amount) {
    if (amount > 1000) return amount * 0.1;
    return 0;
}

// ServiceB -- same logic, copied and forgotten
double computeDiscount(double value) {
    if (value > 1000) return value * 0.1;
    return 0;
}

When the business rule changes (threshold becomes 1500, rate becomes 12%), one copy gets updated and the other does not. Two modules now disagree on fundamental business logic, and the discrepancy surfaces in production during an audit rather than in testing.

Fix: Extract the shared logic into a single function, utility class, or shared library that both callers reference.

Long Method

A method that has grown beyond its original purpose by absorbing additional responsibilities over time. The cognitive load of reading a 200-line method is qualitatively different from reading twenty 10-line methods, not just quantitatively. Long methods are hard to test because they perform too many things to test in isolation, and hard to understand because the reader must hold the entire execution context in working memory.

Detection threshold: Methods above 20-30 lines warrant review; above 50 lines, refactoring is almost always justified. In COBOL, paragraphs exceeding 100 statements are the equivalent.

python

class OrderProcessor:
    def process_order(self, order):
        # Validate order -- 40 lines
        # Calculate discounts -- 30 lines
        # Update inventory -- 25 lines
        # Send notification emails -- 20 lines
        # Generate invoice -- 35 lines
        # 150+ lines total
        pass

Each responsibility in this method should be a separate class or function. Bundling them means every future update to invoicing, inventory, or notifications risks destabilizing the entire order processing flow.

God Class

A class that has accumulated responsibilities across multiple domains, violating the Single Responsibility Principle so severely that it becomes the center of a codebase’s gravity: everything depends on it, and changing anything in it requires understanding everything about it.

Detection signal: A class with more than 20-30 public methods, or one whose name contains “Manager,” “Processor,” “Handler,” “Utils,” or “Helper” applied to multiple unrelated domains.

Divergent Change

A class that is modified for many different, unrelated reasons. Every time the database schema changes, you edit this class. Every time the pricing rules change, you edit this class. Every time the notification format changes, you edit this class. This class has too many responsibilities and should be split.

Definition: One class that keeps changing for different reasons. The inverse of Shotgun Surgery.

Shotgun Surgery

A single conceptual change that requires edits across many different classes. Changing a tax rate requires modifying a backend calculation, a frontend validation, a database trigger, a batch job, and a reporting query, in five different places. Missing any one produces inconsistent behavior.

sql

-- Tax logic duplicated across queries
SELECT amount * 0.05 FROM invoices;
SELECT amount * 0.05 FROM payments;
SELECT amount * 0.05 FROM reports;

Changing 0.05 to 0.07 now requires finding every occurrence across SQL files, stored procedures, and application code.

Feature Envy

A method that spends more time using the data and methods of another class than its own. This signals that the behavior probably belongs in the other class.

java

// In ReportGenerator -- envious of Customer's data
double calculateCustomerRating(Customer customer) {
    return customer.getOrderCount() * customer.getAverageOrderValue()
           / customer.getDaysSinceRegistration();
}
// This logic belongs in Customer, not ReportGenerator

Dead Code

Code that exists in the repository but is never called by any execution path in production. Dead code accumulates over years as features are removed, replaced, or restructured without deleting the old code. It adds noise to code review, confuses developers onboarding to the codebase, complicates migration analysis, and occasionally gets accidentally reactivated.

Detection: Static analysis tools including SonarQube, Knip (for TypeScript/JavaScript), and SMART TS XL identify unreachable functions, uncalled methods, and unused variables across the codebase.

DRY Principle Violations

The Don’t Repeat Yourself (DRY) principle states that every piece of knowledge must have a single, unambiguous representation within a system. DRY violations are the root cause of Duplicated Code, Data Clumps, and many Shotgun Surgery scenarios. When business logic is represented in multiple places, those representations inevitably diverge. DRY is the principle; Duplicated Code is the smell that indicates the violation.

python

# DRY violation: same validation logic in three places
def validate_email_in_registration(email):
    return "@" in email and "." in email

def validate_email_in_profile_update(email):
    return "@" in email and "." in email

def validate_email_in_checkout(email):
    return "@" in email and "." in email

# DRY-compliant: one function, three callers
def is_valid_email(email):
    return "@" in email and "." in email

Detection Thresholds: When Does Code Become a Smell?

Code smell detection requires measurable thresholds. Below are the commonly used metrics and the values that indicate a smell requiring attention:

MetricWhat It MeasuresWarning ThresholdCritical Threshold
Cyclomatic ComplexityNumber of decision branches in a methodAbove 10Above 20
Method Length (lines)Number of lines in a method/functionAbove 20Above 50
Parameter CountNumber of parameters a method acceptsAbove 4Above 7
Class LengthNumber of lines in a classAbove 200Above 500
Duplication RatePercentage of code that is duplicatedAbove 3%Above 10%
Cognitive ComplexityHow difficult the code is to understandAbove 15Above 25
Afferent Coupling (Ca)Number of classes that depend on this classAbove 15Above 30
Efferent Coupling (Ce)Number of classes this class depends onAbove 15Above 30

These thresholds are configurable in SonarQube, and most static analysis platforms allow custom rules based on these metrics. The critical-threshold classes and methods are the highest-priority refactoring targets: they are the most likely sources of future defects and the most expensive components to maintain.

Code Smell Detection Tools

Automated detection is the only scalable approach for identifying code smells across large codebases. Manual review catches a fraction of what automated tools find, and manual review does not scale to legacy systems with millions of lines.

ToolPrimary LanguageWhat It Detects
SonarQube / SonarCloudJava, Python, JS/TS, C#, and moreFull Fowler smell taxonomy, security hotspots, duplications
Checkstyle + PMDJavaStyle violations, duplications, complexity metrics
ESLint + typescript-eslintJavaScript, TypeScriptLong functions, complexity, unused code
Pylint + RadonPythonComplexity, style, maintainability index
ReSharper / RiderC#Redundant code, long methods, coupling issues
ClippyRustIdiomatic violations, common patterns that are code smells in Rust
CodeClimateMulti-languageComplexity, duplication, maintainability score
SMART TS XLCOBOL, JCL, Java, Python, RPG, SQL, .NETCross-language duplication, dead code, coupling, dependency drift

Code smells in Rust are caught primarily by Clippy, which enforces idiomatic Rust patterns. The most common Rust-specific smells include unnecessary cloning, misuse of unwrap() in production paths, overly nested match expressions, and functions that should return Result but use panics instead.

Code Smells and Technical Debt: The Connection

Technical debt is the accumulated cost of past decisions that favored speed over quality. Code smells are the mechanism by which that debt manifests in code structure. The relationship is direct: each unaddressed code smell is a unit of technical debt, and the interest rate is the extra time every future change must spend working around it.

As described in the context of impact analysis for software change management, the structural problems that code smells indicate, excessive coupling, duplicated logic, dead code accumulation, directly increase the scope of every change because they make it harder to isolate what any given change will affect.

Explain technical debt in terms of code smells: if a codebase has 40% duplication, every bug fix costs 1.4x what it should. If the core processing class is a God Class that everything depends on, every feature addition requires understanding and testing the entire class. If error handling is inconsistent, every production incident requires more investigation time because the failure signals are unreliable. Technical debt is not abstract, it is the sum of these compounding inefficiencies.

The CISQ research consistently finds that developers spend 30-40% of their time working around technical debt rather than building new functionality. Code smell density is the most direct measurement of how much debt has accumulated.

How SMART TS XL Detects Code Smells at Enterprise Scale

Individual tools like SonarQube and Clippy operate within a single language. In enterprise environments where COBOL programs write to datasets that Java services read, where JCL job streams invoke programs in multiple languages, and where the same business logic has been independently duplicated across three different systems written in three different decades, single-language tools cannot see the full picture.

SMART TS XL’s static code analysis detects code smells across every language in the environment simultaneously: duplicated logic between a COBOL copybook and a Java utility class, dead code in RPG programs that no JCL job invokes, God Class patterns in COBOL programs where a single paragraph does the work of fifty, and inconsistent error handling patterns across the cross-language boundary.

The application dependency mapping capability identifies the architectural smells that individual file-level tools cannot see: which components have the highest afferent coupling (most depended-upon, highest risk of breaking things when changed), where circular dependencies exist between modules that should be independent, and where duplicated business logic has been independently maintained in different systems without either copy being aware of the other.

The impact analysis capability makes smells actionable: before refactoring any high-coupling component, the impact analysis enumerates every dependent component that needs to be tested, validated, or updated. This transforms the “refactoring paralysis” that teams experience in large smelly codebases into a structured, scoped remediation program where each change has a defined scope rather than unknown risk.

For teams conducting legacy modernization programs, code smell analysis is the foundation of the modernization plan: the dead code is eliminated before migration begins (reducing the scope), the duplicated logic is consolidated into canonical implementations, the highest-coupling components are modernized last (after everything that depends on them has been addressed), and the God Classes are decomposed before being converted to a new language, because converting a God Class to Java produces a God Class in Java.

Addressing Code Smells: A Prioritization Framework

Not all code smells warrant immediate refactoring. The right approach is risk-based prioritization:

Priority 1, Smells in high-change-rate components. Code that changes frequently and has high complexity or coupling produces the most defects. These components cost the most per change and generate the most production incidents. Fix these first.

Priority 2, Smells at architectural boundaries. God Classes and high-coupling components that everything depends on are the most dangerous to change but also the most expensive to leave unfixed. These require the most careful impact analysis before refactoring.

Priority 3, Duplicated code across system boundaries. When the same business logic exists in multiple systems, changes must be coordinated across all copies simultaneously. Consolidating this duplication reduces coordination overhead and prevents divergence.

Priority 4, Dead code removal. Dead code is the safest category to address: removing it cannot break behavior, only reveal previously hidden dependencies. It should be removed before any migration or conversion to avoid wasted effort converting code that will never be called.

Priority 5, Style and structural smells in low-risk areas. Long methods and parameter lists in stable, low-change-rate code can be addressed opportunistically, when nearby code needs to change for other reasons, refactor the surrounding smells at the same time.

The discipline of detecting, measuring, and addressing code smells systematically, rather than reactively, when a smell has already caused a production failure, is what distinguishes development teams that maintain delivery velocity over time from those that slow down progressively as their systems grow.