Code Smells That Beg for Patterns
Code smells are Fowler's term for surface symptoms of deeper design problems.
Introduction
Code smells are Fowler's term for surface symptoms of deeper design problems. They are not bugs — the code works. They are warnings that the next change will be expensive. A skilled engineer reads smells the way a doctor reads symptoms: each one points to a likely diagnosis, and each diagnosis suggests a specific treatment.
This lesson catalogs the twelve smells most likely to point at a pattern refactor — Long Method, Switch Statement, Feature Envy, Shotgun Surgery, and more — and maps each to the pattern that resolves it. The map is not one-to-one; judgment still applies. But smells radically narrow the candidate list.
The story
A three-engineer team owned a 4,200-line OrderProcessor. Every new feature touched it; every release had bugs. They ran a smell audit in one afternoon, listed eight distinct smells, and mapped each to a refactor. Six weeks later the file became eleven focused classes — Strategy for pricing, State for order status, Observer for notifications, Command for actions. Defect rate dropped 70%. The code did not fail before the audit. The smells predicted that it would.
The business problem
Ignoring code smells does not cause immediate outages — it causes slow, expensive change that compounds:
- Merge conflict tax: god-files with twelve contributors block every parallel feature branch.
- Bug recurrence: the same conditional branch breaks every release because change ripples unpredictably.
- Review paralysis: reviewers cannot reason about 400-line methods — bugs slip through.
- Refactor fear: teams avoid touching smelly modules, so workarounds multiply around them.
- Onboarding cliff: new engineers need weeks to understand one file that should be eleven.
The problem teams faced
These structural symptoms appear in almost every codebase that outgrew its first design:
- Long Method — 50+ lines hiding multiple responsibilities reviewers cannot hold in memory.
- Switch on Type — growing if/else or switch chains signal missing polymorphism (Strategy, State).
- Shotgun Surgery — one business change requires editing eight files; missing Facade or Mediator.
- Feature Envy — a method reads another class's data more than its own; behavior is misplaced.
- Divergent Change — one class changes for unrelated reasons; violates Single Responsibility.
- Speculative Generality — abstractions built for futures that never arrived (see previous lesson).
Understanding the topic
Four-step smell reading. For every smell you spot, walk this chain before touching code:
- 1. Name the smell — use Fowler's vocabulary so the team shares diagnosis language.
- 2. Diagnose the force — what design problem does this smell usually mean?
- 3. Pick the refactor move — Extract Method, Move Method, Replace Conditional with Polymorphism.
- 4. Reach for the pattern — only when the refactor move points to a recurring variation axis.
Internal architecture
Smell → pattern reference map — bookmark this for code review and smell audits:
Smell → Likely pattern / refactor──────────────────────────────────────────────────────────────Switch on type → Strategy / State / VisitorLong Method → Extract Method → CommandFeature Envy → Move Method → MediatorShotgun Surgery → Facade / MediatorData Clumps → Value Object / BuilderPrimitive Obsession → Value ObjectDivergent Change → Single Responsibility / StrategyParallel Inheritance → BridgeInappropriate Intimacy → Mediator / FacadeSpeculative Generality → Delete abstraction (not a pattern!)Duplicate Code → Extract Method → Template Method
Visual explanation
Three diagrams guide smell-driven refactoring. The first is the diagnostic pipeline. The second maps the most common smells to likely patterns. The third shows how to prioritize — fix high-churn hotspots first, not the prettiest violations.
Informative example
Smell: Switch on Type — every new carrier adds another case; merge conflicts guaranteed:
// ❌ Switch on type — smells like missing Strategyfunction ship(order: Order, carrier: string) {switch (carrier) {case "fedex": return fedexApi.createShipment(order);case "ups": return upsApi.createShipment(order);case "dhl": return dhlApi.createShipment(order);default: throw new Error(`Unknown carrier: ${carrier}`);}}// ✅ Strategy — new carrier = new class, zero edits to ship()interface CarrierStrategy {createShipment(order: Order): Promise<ShipmentId>;}function ship(order: Order, carrier: CarrierStrategy) {return carrier.createShipment(order);}
// ❌ Feature envy — InvoicePrinter knows too much about Order internalsclass InvoicePrinter {print(order: Order) {return `${order.customer.name} · ${order.lineItems.length} items · ${order.total.cents}`;}}// ✅ Move Method — behavior lives with the data it usesclass Order {invoiceSummary() {return `${this.customer.name} · ${this.lineItems.length} items · ${this.total.cents}`;}}
Smell: Feature Envy — InvoicePrinter reads five fields from Order that belong on Order itself. Fix with Move Method first; reach for Mediator only if coordination spans three or more classes.
Execution workflow
Audit the file
Read the suspect module top-to-bottom; list smells by Fowler's names.
Real-world use
SonarQube, CodeClimate, and ESLint flag many smells automatically — but tools catch syntax-level signals, not domain-level ones. A 200-line SQL builder may trigger "Long Method" falsely; a 40-line method with switch-on-type may pass every metric. Mature teams combine tool output with manual smell audits before major feature work. Treat smells as a backlog item ranked by git churn, not aesthetic offense.
Production case study
A logistics platform's routing module had 3,800 lines, 42 if-statements, and eighteen contributors:
- Symptoms: every feature triggered merge conflicts; bug-fix lead time averaged 9 days.
- Smell audit: Switch on Type (carrier), Shotgun Surgery (notifications), Feature Envy (pricing).
- Refactor plan: Strategy for carriers, Observer for notifications, Move Method for pricing — four weeks with characterization tests.
- Outcome: one god-file became eight focused classes; merge conflicts dropped 80%; bug lead time fell to 2 days.
- Lesson: name smells in the PR that triggered the audit — depersonalize feedback, accelerate consensus.
Trade-offs
- Pro — Diagnostic vocabulary: smells turn vague "this is ugly" into actionable backlog items.
- Pro — Pattern selection: each smell narrows candidates to two or three patterns.
- Pro — Review consistency: cite smell names in PR comments instead of personal taste.
- Con — False positives: Long Method is sometimes correct (parsers, SQL builders, flat config).
- Con — Smell fatigue: auditing everything demoralizes — pick three high-impact smells per quarter.
- Con — Cosmetic fixes: renaming without fixing the underlying force wastes a sprint.
Decision framework
- Audit before major features in modules with high git churn — refactor first, then build.
- Audit after incidents — postmortems often trace to a smell the team ignored for months.
- Cite smells in code review — "Switch on Type here" depersonalizes feedback.
- Do not witch-hunt — chasing every smell paralyzes delivery.
- Do not refactor without tests — characterization tests on legacy code first, or you are gambling.
- Do not refactor code being deleted — smell audits on modules with six-month sunset are waste.
Best practices
- Smells fixed by refactor moves alone: Duplicate Code → Extract Method; Long Parameter List → Introduce Parameter Object — no pattern needed.
- Smells that need patterns: Switch on Type with 3+ variants; Shotgun Surgery across 5+ files; Divergent Change with unrelated change reasons.
- Prioritize: impact score = git churn × number of contributors touching the file.
- Run `git log --follow -p -- path/to/file` before debating — evidence ends arguments.
- Document smell → pattern decisions in ADRs so the team inherits the diagnosis.
- Celebrate smell audits like features — schedule them, timebox them, measure outcomes.
Anti-patterns to avoid
- Splitting a readable 200-line flat sequence into fifteen one-liner methods "to fix Long Method."
- Introducing Strategy when two variants exist and the second is a one-line edge case.
- Running SonarQube cleanup sprints without prioritizing by business impact.
- Calling something a smell because you dislike the author's style — smells are structural, not personal.
Common mistakes
- Refactoring during unrelated feature work without a dedicated time budget.
- Skipping characterization tests — smells become incidents mid-refactor.
- Fixing smells in modules about to be replaced by a vendor SDK or microservice extraction.
- Assuming every smell needs a GoF pattern — many need a simple Extract Method.
Debugging tips
- If refactor did not reduce churn, the seam was wrong — re-run git log and find the real change axis.
- When tests fail after Extract Class, the class boundary split behavior from data incorrectly.
- High cyclomatic complexity in one method often means Switch on Type — count the branches.
- Use `git shortlog -sn -- path` to see who touches a smelly file — prioritize shared pain.
Optimization strategies
- Batch smell fixes in one module per sprint — context switching kills refactor momentum.
- Pair smell audits — one person reads, one person names smells aloud; catches blind spots.
- Keep a team smell backlog in your issue tracker, tagged separately from bugs.
- For interviews, prepare one story: smell you spotted → diagnosis → pattern → measurable outcome.
Common misconceptions
- Many developers believe smells mean the code is broken. In reality, smells mean the next change will be expensive — tests still pass.
- Many developers believe every Long Method must be split. In reality, cyclomatic complexity and change frequency matter more than line count.
- Many developers believe tools replace manual smell reading. In reality, domain smells (Feature Envy, Divergent Change) need human judgment.
- Related: Fowler's Refactoring catalog — the refactor moves; patterns are the destinations.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionName three code smells and the patterns they typically lead to.+
Answer
Follow-up
2AdvancedQuestionWhen is a long method actually fine?+
Answer
Follow-up
3AdvancedQuestionHow do you prioritize which smells to fix?+
Answer
Follow-up
4AdvancedQuestionHave you refactored toward a pattern and made things worse?+
Answer
Follow-up
5BeginnerQuestionWhat is the difference between a smell and a bug?+
Answer
Follow-up
Summary
You can now read code smells like symptoms: name them, diagnose the force, match to a refactor or pattern, and prioritize by impact. Tell the OrderProcessor story and walk through the Switch-on-Type example — if you can do that without notes, you own this lesson.