Cost models illustrative. Numbers from academic and industry research; methodology documented. Not investment or engineering advice; your mileage will vary.
last verified April 202622 min read

The 22 code smells in Fowler's catalog, with a dollar figure attached to each

Martin Fowler and Kent Beck first codified the smell catalog in Refactoring (1999, Addison-Wesley). The 2018 second edition refined and expanded it. Five categories organise the 22 smells: Bloaters (code that has grown too large), Change Preventers (code that resists modification), Couplers (code with excessive coupling), Dispensables (code that should not exist), and OO Abusers (code that misuses object-orientation).

The catalog is not a law. Prototype code, expiring code, and deadline-bound code are legitimate contexts where smells are defensible choices, not failures. Read /when-smells-are-ok for the honest counterpoint.

Cost estimates below are annual figures for a team of eight engineers. Ranges reflect low-severity (single violation, partial test coverage) to high-severity (multiple violations, no tests). Methodology documented at /calculator.

Also worth reading: Ousterhout's A Philosophy of Software Design as the minority report against some of this catalog's implications. He argues for deep modules rather than small ones. Full reading list →

Bloater

1. Long Method

$22K–$80K / yr / yr

A method that has grown to the point where it is doing too much. Fowler (ch. 3): the longer a procedure, the harder it is to understand. The eye must travel too far, the context must be held in working memory for too long.

// 280-line method doing validation, business logic, and persistence
function processOrder(order) {
  // validation (lines 1-60)
  // pricing logic (lines 61-140)
  // inventory check (lines 141-200)
  // persistence (lines 201-280)
}

Why it costs money: Cognitive Complexity (Campbell 2018) scales with nesting and branching, not raw LOC. A 40-line method with three nested conditionals can have a higher CC than a 120-line flat switch. High-CC methods correlate with longer review times and higher defect rates.

Refactoring: Extract Method (Fowler ch. 6). Modern IDEs do 90% of the mechanical extraction; the human work is naming and boundary selection.

Detect with: SonarCloud: cognitive-complexity rule, max-lines-per-function. ESLint: complexity, max-lines-per-function. JetBrains: built-in method-length warnings.

Read the full deep-dive →
Bloater

2. Large Class / God Class

$42K–$180K / yr / yr

A class that has accumulated too many responsibilities. Often recognisable by a name containing 'Manager,' 'Handler,' 'Processor,' or 'Service.' A God Class knows too much, does too much, and is modified by too many engineers for too many reasons.

// 1,400-line UserManager class with 52 methods:
// authentication, profile management, notification logic,
// subscription billing, audit logging, and report generation
class UserManager { ... }

Why it costs money: Bird et al. (FSE 2011): modules with fragmented ownership have 2-3x the defect rate. A God Class attracts fragmented ownership by definition. Basili 1996: coupling (CBO) correlates strongly with defect density. A God Class creates coupling across every consumer.

Refactoring: Extract Class (Fowler ch. 6). Move Method (ch. 9). Replace Conditional with Polymorphism (ch. 10). Budget multiple sprints; do not attempt in one.

Detect with: SonarCloud: class-complexity and too-many-methods rules. CodeScene: hotspot map shows God Classes by change frequency. Sourcegraph: batch ownership view.

Read the full deep-dive →
Bloater

3. Primitive Obsession

$8K–$30K / yr / yr

Using primitive types (strings, ints, booleans) where a small class or value object would carry meaning, validation, and invariants. A phone number stored as a string validates nothing. A currency amount stored as a float loses precision.

// Primitive obsession
function createUser(name: string, phone: string, currency: string, amount: number) {}

// Better: value objects carry validation
function createUser(name: Name, phone: PhoneNumber, price: Money) {}

Why it costs money: Primitive obsession tends to co-occur with duplicate validation logic scattered across the codebase. When the rules change (a phone number now requires country code), every validation site must be updated. Each missed site is a bug.

Refactoring: Replace Primitive with Object (Fowler ch. 7). Introduce Value Object. Especially valuable for money, phone numbers, postal codes, email addresses.

Detect with: SonarSource: custom rules for repeated string literals. Manual review: grep for validation patterns applied to raw strings.

Bloater

4. Long Parameter List

$8K–$35K / yr / yr

More than three or four parameters is a warning sign. Callers must remember the parameter order. Positional parameters are easy to swap accidentally. Boolean flags in parameter lists often indicate a method doing two things.

// Caller must remember all seven positions
createPayment(userId, amount, currency, method, retry, notify, auditTag);

// Better: parameter object
createPayment({ userId, amount, currency, method, options })

Why it costs money: Long parameter lists are brittle. Adding a parameter requires changing every call site. Boolean flag parameters (isRetry: boolean) signal that the method should be two methods.

Refactoring: Introduce Parameter Object (Fowler ch. 6). Preserve Whole Object. If you find yourself passing subsets of an object's data, pass the object.

Detect with: SonarCloud: max-params rule. ESLint: max-params. Manual: grep for function signatures with >3 comma-separated parameters.

Bloater

5. Data Clumps

$6K–$25K / yr / yr

Three or more fields that appear together in multiple classes and parameter lists. Latitude and longitude. Start date and end date. Host, port, and protocol. These data clumps want to be a class.

// Data clump appearing in multiple places
function connectToDatabase(host: string, port: number, dbName: string, timeout: number) {}
function testDatabaseConnection(host: string, port: number, dbName: string) {}

// Better: DatabaseConnection value object

Why it costs money: Data clumps lead to inconsistent handling. Some callers validate the combination; others do not. Extracting into a class centralises the invariants.

Refactoring: Extract Class. Replace Data Value with Object. Move any relevant methods to the new class.

Detect with: Manual review: search for the same set of field names appearing across multiple class definitions and method signatures.

Change Preventer

6. Divergent Change

$15K–$60K / yr / yr

A class that is changed for multiple unrelated reasons. If you find yourself changing the same class every time a new database schema is added AND every time a new product category is added, the class has divergent change. It is doing two jobs.

// OrderProcessor changed for 3 unrelated reasons:
// - new payment provider added
// - new shipping region added
// - new tax calculation rule added

Why it costs money: Divergent Change is the code-level symptom of a Single Responsibility Principle violation. The class has a narrow test surface for a wide set of behaviours. Changes to one responsibility risk breaking another.

Refactoring: Extract Class for each axis of change. Move the responsibilities into separate classes with single, clear purposes.

Detect with: CodeScene: classes with high change frequency across multiple independent features. Review git log: if a file is touched in commits for unrelated tickets, it has divergent change.

Change Preventer

7. Shotgun Surgery

$20K–$75K / yr / yr

The inverse of Divergent Change. One logical change requires editing many classes. Add a new notification type and you have to touch the notification factory, the user preferences model, the email template renderer, the SMS adapter, and three test fixtures.

// Adding a new user role requires changes in:
// - AuthorizationService
// - UserRepository
// - PermissionsMatrix
// - AuditLogger
// - UI RoleSelector component
// - Database migration
// - 6 test files

Why it costs money: Shotgun Surgery is expensive because the developer must hold the full change map in their head. Missing one class means a silent bug. The risk compounds when changes cross team boundaries.

Refactoring: Move Method and Move Field to colocate the related behaviour. Inline Class if a class has too little behaviour after the move.

Detect with: CodeScene: 'code health' deterioration correlated across multiple files in the same commits. Manual: git log review for commits touching many files for single-ticket changes.

Coupler

8. Feature Envy

$12K–$55K / yr / yr

A method that spends more time accessing data from another class than its own. The method envies the data of a different class. The canonical signal: a method on class A that makes more calls to class B's getters than A's own fields.

class Order {
  // This method envies Customer's data
  calculateShippingCost(customer: Customer) {
    const zip = customer.getPostalCode();
    const tier = customer.getMembershipTier();
    const region = customer.getRegion();
    // ... uses no Order fields at all
  }
}

Why it costs money: Feature Envy is the coupling smell with the strongest empirical defect correlation after God Class. Basili 1996, Kitchenham 2004, and Rahman 2025 all find coupling metrics predict defect density across OO systems.

Refactoring: Move Method (Fowler ch. 9). Move the envious method to the class it envies. Often the cleanest refactor in the catalog.

Detect with: SonarCloud: Feature Envy rule (Java, C#). Manual: grep for methods where >60% of field/method calls reference a class that is not the containing class.

Read the full deep-dive →
Dispensable

9. Data Class

$4K–$15K / yr / yr

A class with only fields and getters/setters. No behaviour of its own. Data Classes are often fine as value objects, but when they accumulate fields and attract callers that do all the logic, they signal Feature Envy in the callers.

class OrderItem {
  private productId: string;
  private quantity: number;
  private unitPrice: number;
  getProductId() { return this.productId; }
  getQuantity() { return this.quantity; }
  getUnitPrice() { return this.unitPrice; }
  // No business logic whatsoever
}

Why it costs money: Data Classes are not inherently bad. The problem is when logic that should live in the class is instead scattered across multiple callers, each implementing the same calculation differently.

Refactoring: Move Method: pull the scattered logic back into the Data Class. Encapsulate Field. If the class genuinely models a value, keep it pure and add an equality method.

Detect with: SonarSource: anemic domain model warnings. Manual: grep for classes with only get/set methods and no non-trivial methods.

OO Abuser

10. Refused Bequest

$8K–$30K / yr / yr

A subclass that ignores or overrides everything it inherits from its parent. The subclass does not want the parent's interface; the hierarchy is wrong. The subclass is using inheritance for code reuse rather than for subtyping.

class Stack extends ArrayList {
  // Stack refuses most of ArrayList's interface:
  // add(index, element) - wrong for a stack
  // set(index, element) - wrong for a stack
  // Only push() and pop() make sense

Why it costs money: Refused Bequest violates the Liskov Substitution Principle. Code that accepts a parent and receives a subclass can break in unexpected ways. The hierarchy communicates a lie.

Refactoring: Replace Inheritance with Delegation (Fowler ch. 12). The subclass holds a reference to the parent-class behaviour it actually uses and delegates to it.

Detect with: Manual: find subclasses that override most parent methods with empty bodies or throw UnsupportedOperationException.

Dispensable

11. Duplicate Code

$18K–$95K / yr / yr

Fowler called it 'number one in the stink parade.' If you see the same code structure in more than one place, the codebase will be better if you find a way to unify them. The DRY principle (Don't Repeat Yourself) was coined by Hunt and Thomas in The Pragmatic Programmer for exactly this reason.

// Same discount calculation in three places:
// OrderService.calculateDiscount()
// InvoiceService.applyDiscount()
// ReportService.getDiscountedTotal()
// Each slightly different. One has been updated; two haven't.

Why it costs money: Spinellis 2006 estimated 30-50% of real-world codebases contain duplicate code at some level of abstraction. The cost is Shotgun Surgery: fix a bug in one location and forget the other two. Silent defects persist.

Refactoring: Extract Method for duplicate logic in the same class. Extract Class for shared logic between classes. Pull Up Method for duplication in sibling subclasses.

Detect with: PMD CPD (Copy-Paste Detector): best-in-class for cross-file duplicate detection. SonarCloud: duplication metrics. CodeScene: copy-paste lines metric. JetBrains: built-in duplicates inspection.

Read the full deep-dive →
Dispensable

12. Dead Code

$10K–$35K / yr / yr

Code that is never executed. Dead methods, dead parameters, dead classes, dead feature flags. Knight Capital's $440M loss on 1 August 2012 is the canonical case: an unused code path was re-enabled by a deployment flag repurposed for new behaviour. Dead code is expensive inventory.

// Feature flag from 2019, never removed
if (process.env.ENABLE_LEGACY_PRICING === 'true') {
  // This path was last used in production 3 years ago.
  // It is still here. It will surprise someone.
}

Why it costs money: Dead code creates maintenance confusion (is this intentional? tested?), hides latent bugs (the Knight Capital path was dormant, not deleted), and inflates cognitive load during reviews.

Refactoring: Delete it. Use version control; it is not lost. Enable dead-code detection in your linter and block merges that introduce it.

Detect with: SonarCloud: unused variable/method rules. TypeScript: noUnusedLocals/noUnusedParameters. Webpack/Rollup tree-shaking for unused imports.

Dispensable

13. Speculative Generality

$10K–$40K / yr / yr

Hooks, base classes, parameters, and abstractions added for hypothetical future requirements that never materialised. 'We might need to support multiple currencies eventually.' The eventual never came. The abstraction remains, carrying complexity for zero current benefit.

// A six-level abstraction hierarchy for a feature
// that has exactly one implementation:
AbstractPaymentProviderFactory
  -> PaymentProviderFactory
    -> ConcretePaymentProviderFactory
      -> StripePaymentProviderFactory (the only one)

Why it costs money: YAGNI (You Aren't Gonna Need It) - the XP principle against speculative generality. Over-general abstractions are harder to understand, harder to modify, and create false confidence that extension is easy.

Refactoring: Collapse Hierarchy. Inline Class. Remove abstract parameter if only one value is ever passed. Simplify until the code reflects only what is actually needed.

Detect with: Manual: find abstract classes with a single concrete implementation. Find parameters that are always called with the same value.

Dispensable

14. Lazy Class / Lazy Element

$3K–$12K / yr / yr

A class that does so little it should be collapsed into its caller or the class it delegates to. Often the result of a refactoring that extracted too aggressively, or a class added 'just in case' that never grew.

class UserEmailFormatter {
  format(email: string): string {
    return email.toLowerCase().trim();
  }
}
// This is one line. It does not need to be a class.

Why it costs money: Lazy classes cost attention. Every class in a codebase is something a reader has to discover, understand, and relate to other classes. A class that does nothing meaningful is noise.

Refactoring: Inline Class (Fowler ch. 7). If the class is a subclass with very little behaviour, consider Collapse Hierarchy.

Detect with: Manual: any class with fewer than three non-trivial methods. SonarCloud: cyclomatic complexity 1 across all methods in a class.

Dispensable

15. Alternative Classes with Different Interfaces

$8K–$28K / yr / yr

Two classes that do the same job with different method names and signatures. Often the result of parallel development or a team not discovering an existing class. The user has to know which one to use and when.

// Two email services created independently:
class EmailService {
  send(to: string, subject: string, body: string): Promise<void>
}
class NotificationMailer {
  dispatch(recipient: string, template: string, data: object): void
}

Why it costs money: Alternative Classes violate the Don't Repeat Yourself principle at the interface level. When one of them is updated (new retry logic), the other falls behind.

Refactoring: Rename Method to align interfaces. Move Method. Extract Superclass or Interface. Then remove one implementation.

Detect with: Manual: grep for classes with similar method names across different modules. CodeScene: coupling analysis reveals duplicate-behaviour clusters.

OO Abuser

16. Temporary Field

$5K–$18K / yr / yr

An instance variable that is only set in certain execution paths. The object is initialised with null for most of its lifetime and suddenly populated in one method. Code reading the object must check whether the field is set.

class OrderProcessor {
  private lastOrderId: string | null = null; // Only set during processOrder()
  private processingStartTime: Date | null = null; // Only during active processing

  processOrder(order: Order) {
    this.lastOrderId = order.id; // Temporary, not always set
  }
}

Why it costs money: Temporary Fields create a class that behaves differently depending on execution context. This violates the principle of least surprise and makes objects harder to test.

Refactoring: Introduce Null Object for the null case. Extract the special-case logic into a separate method or class that receives the field as a parameter.

Detect with: Manual: fields initialised to null in the constructor and set elsewhere. TypeScript strict null checks catch many patterns.

Coupler

17. Message Chains

$6K–$22K / yr / yr

a.getB().getC().getD() - a chain of calls that requires the caller to traverse multiple object relationships. This is the 'Law of Demeter' violation: a caller should only talk to its immediate friends, not friends of friends.

// Caller knows the entire object graph:
const city = order.getCustomer().getAddress().getCity();

// Preferred:
const city = order.getDeliveryCity(); // Order encapsulates the traversal

Why it costs money: Message chains couple the caller to the internal structure of the entire chain. If Address is restructured to hold a PostalCode object instead of a string, every chain traversal breaks.

Refactoring: Hide Delegate (Fowler ch. 7). Move the traversal into a method on the entry-point class. The caller no longer needs to know the object graph.

Detect with: Manual: grep for lines containing three or more chained method calls. SonarCloud: 'demeter' custom rules.

Coupler

18. Middle Man

$4K–$15K / yr / yr

A class that delegates everything to another class and adds no value itself. Often the residual stump after aggressive extraction refactoring. The Middle Man sits in the call path, adding a hop without adding behaviour.

class OrderFacade {
  private orderService: OrderService;
  processOrder(order) { return this.orderService.processOrder(order); }
  cancelOrder(id) { return this.orderService.cancelOrder(id); }
  getOrderStatus(id) { return this.orderService.getOrderStatus(id); }
  // Every method is a pure delegation
}

Why it costs money: Middle Men add indirection without abstraction. They confuse readers ('why is this here?'), inflate class counts, and add latency in performance-sensitive paths.

Refactoring: Remove Middle Man (Fowler ch. 7). If the class is a thin wrapper with no invariants, delete it and call the delegate directly.

Detect with: Manual: classes where every method delegates to a single other class. Code coverage tools: classes with 100% coverage but no logic of their own.

Coupler

19. Inappropriate Intimacy

$12K–$45K / yr / yr

Two classes that poke into each other's private parts. Each has strong knowledge of the other's internal implementation. Change one and the other breaks. The coupling is bidirectional and implicit.

class Order {
  customer: Customer; // public, accessed directly

  // Directly accesses Customer internals:
  getTaxRate() {
    return this.customer.taxTable[this.customer.region].defaultRate;
  }
}

Why it costs money: Inappropriate Intimacy creates circular dependencies that make independent testing impossible. The two classes must always be deployed and changed together, defeating the purpose of encapsulation.

Refactoring: Move Method and Move Field to reduce the intimacy. Extract Class to separate shared concerns. Change Bidirectional Association to Unidirectional.

Detect with: SonarCloud: coupling-between-classes rule. Manual: classes that appear together in every git commit. CodeScene: temporal coupling analysis.

Change Preventer

20. Parallel Inheritance Hierarchies

$15K–$55K / yr / yr

Adding a subclass in one hierarchy forces the addition of a subclass in another. Every time you create a new PaymentMethod subclass, you must also create a PaymentMethodValidator, PaymentMethodLogger, and PaymentMethodAuditRecorder.

// Adding CryptoPayment requires creating:
// - CryptoPayment (payment hierarchy)
// - CryptoPaymentValidator (validator hierarchy)
// - CryptoPaymentLogger (logger hierarchy)
// - CryptoPaymentAuditRecord (audit hierarchy)

Why it costs money: Parallel hierarchies compound the cost of every extension. They also frequently violate Open-Closed: adding a new type requires changing multiple base classes.

Refactoring: Use Move Method and Move Field to concentrate the logic in one hierarchy. Apply the Visitor pattern if the multiple-dispatch is intentional.

Detect with: Manual: find hierarchies where the same names appear at corresponding levels (StripePayment, StripePaymentValidator). CodeScene: coupling analysis.

Dispensable

21. Comments as Apology

$3K–$10K / yr / yr

Comments that explain why the code is bad rather than what it does. 'This is a hack.' 'I know this is wrong, but...' 'Temporary workaround from 2018.' The comment signals that the code should be improved, not documented.

// FIXME: This is a temporary workaround because the API
// doesn't support batch operations. Should be refactored
// when we have time. Created 2021-03-15.
for (const item of items) {
  await api.process(item); // N+1 call in a loop
}

Why it costs money: Apology comments are a useful smell signal: they mark code the author knew was wrong at the time of writing. They do not fix the underlying problem. Good comments explain the 'why' of a decision, not that the code needs fixing.

Refactoring: Fix the code the comment is apologising for. If the fix is not possible now, add a JIRA ticket, mark the technical debt explicitly, and remove the comment.

Detect with: grep: 'FIXME', 'HACK', 'TODO: this is wrong', 'temporary'. SonarCloud: comment smells.

OO Abuser

22. Switch Statements

$10K–$40K / yr / yr

Long switch or if/else chains that are duplicated across the system. The same type-switch on payment method appears in the pricing service, the receipt generator, and the audit logger. Adding a new type requires touching all three.

// Same switch appearing in 5 places:
switch (payment.type) {
  case 'STRIPE': // ...
  case 'PAYPAL': // ...
  case 'BANK': // ...
  default: throw new Error('Unknown');
}

Why it costs money: Switch Statements are a signal of missing polymorphism. Each duplication is a place where a new type will be silently missed. In 2026, the orthodoxy has softened: a single, well-tested switch is often preferable to over-engineered polymorphism. The smell is the repetition across the system.

Refactoring: Replace Conditional with Polymorphism (Fowler ch. 10). Extract each case into a subclass or strategy. The switch remains in one factory; consumers call a polymorphic interface.

Detect with: SonarCloud: switch-statement complexity rules. Manual: grep for switch or if/else chains that appear in multiple classes.

The catalog is necessary but not sufficient

Fowler's catalog remains the best single reference for naming and discussing code problems. But John Ousterhout's A Philosophy of Software Design (Yaknyam Press, 2018) offers an important counterpoint: deep modules beat small scattered ones, and over-decomposed code can be harder to understand than a longer, well-commented method. The field legitimately disagrees.

What to do next: run the calculator with your team's numbers, read the refactoring ROI memo, then pick three smells to actually act on.

God Class deep-diveLong Method deep-diveDuplicate Code deep-diveFeature Envy deep-diveBug rate researchDetection toolsWhen smells are OK