skip to content

Code smells are heuristics rather than rules. How do you decide which detected smells to act on, and when is leaving a smell in place the right engineering decision?

level: principalimportance: nice to knowfreq 41%

answer

  1. value = P(change) × cost per change − risk
  2. hotspots: complex AND frequently changed
  3. make the change easy, then make the easy change
  4. characterisation tests before touching legacy
  5. some smells are deliberate design (Strategy, DTOs, DAMP tests)

basics

~20 s

Fix a smell when it makes upcoming work harder. Weigh how often the code changes, how risky it is, and whether tests protect it. Rarely-touched, stable, or soon-to-be-deleted code can keep its smells — refactoring it costs real money and buys nothing.

solid answer

~60 s

Smells are *leading indicators of future change cost*, so their value depends entirely on whether that future change happens. Prioritise by expected value: - **Change frequency × smell severity.** Hotspots — files with many recent commits and high complexity — dominate the payoff; cold, stable code does not. - **Proximity to upcoming work.** Refactor along the path of the story you are about to do ("refactor to make the change easy, then make the easy change"), not on a separate crusade. - **Safety.** Without characterisation tests, refactoring is risky; write the tests first, or use verified/automated refactorings only. - **Reversibility and blast radius.** Local extractions are cheap and safe; module boundary changes are not. Legitimate reasons to leave a smell: code scheduled for deletion or replacement; generated, vendored, or third-party code; hot paths where the clean structure costs measurable performance; deliberate architectural separations that merely *look* like smells (Strategy is "feature envy", DTOs are "data classes"); duplication that is coincidental; and freeze windows where risk outweighs benefit. Record the decision rather than leaving it implicit, and re-evaluate when the code becomes a hotspot.

go deeper

for a junior

Say that smells are hints, that you fix the ones in code you are about to change, and that you need tests before restructuring.

for a middle

Add prioritisation by change frequency and upcoming work, the boy-scout rule, and separating refactoring commits from behaviour commits.

for a senior

Give the cost/benefit framing, characterisation tests and seams for legacy code, incremental parallel-change sequencing, and concrete cases where a smell should be kept (generated code, hot paths, deliberate designs, coincidental duplication).

for a principal

Frame it as portfolio management: hotspot analysis to allocate effort, thresholds as conversation triggers rather than gates, deliberate-versus-inadvertent debt recorded with revisit criteria, and stakeholder framing that ties restructuring to delivery cost rather than aesthetics.

## Smells are an economic signal A smell predicts that *future* changes to this code will be more expensive or riskier. The value of removing it is therefore: ``` value ≈ P(this code changes again) × (extra cost per change) × (number of future changes) − cost of refactoring − risk of breaking working code ``` Code that never changes again has, by this formula, zero benefit — which is why "clean up the whole codebase" programmes so often fail to pay back. This is the core discipline that separates a principal-level answer from a rule-following one. ## How to prioritise **1. Hotspots.** Combine change frequency (from version control) with a complexity/smell measure. Files that are both complex *and* frequently changed carry nearly all the cost; files that are complex and frozen carry almost none. This is the essence of behavioural code analysis. **2. Proximity to the work in hand.** Kent Beck's rule: *"For each desired change, make the change easy (warning: this may be hard), then make the easy change."* Refactoring earns its keep when it immediately reduces the cost of the story you are delivering. Coupled with the **Boy Scout Rule** (leave the code a little better than you found it), it produces continuous, low-risk improvement without a separate budget. **3. Pain-driven.** Which smells actually cost you last quarter — incidents, long onboarding, repeated omission bugs, merge conflicts? Prefer measured pain over catalogue completeness. **4. Reversibility.** Prefer refactorings that are local, mechanical, and tool-verified (Extract Function, Rename) over sweeping boundary changes. Sequence big restructurings incrementally (parallel change / expand-migrate-contract, strangler-style) so every step is releasable. ## Preconditions for acting safely - **Tests.** Refactoring means *behaviour-preserving* change; without tests you cannot know you preserved it. For legacy code, write **characterisation tests** first — tests that pin current observed behaviour, bugs included — as Michael Feathers describes. Find a **seam**: a place where behaviour can be substituted without editing the code in place. - **Small steps, always green.** Long-lived refactoring branches conflict with feature work and often get abandoned; prefer many small merges. - **Separate commits.** Never mix behaviour change with restructuring in the same commit — it makes review and bisecting far harder. ## Legitimate reasons to leave a smell 1. **Scheduled for deletion or replacement.** Cleaning code you will delete next quarter is pure cost. 2. **Stable and never touched.** A gnarly but correct module untouched for three years is behaving like a black box; leave it alone. 3. **Generated, vendored, or third-party.** Edits are lost on regeneration or upgrade; wrap it behind an interface instead. 4. **Measured performance constraints.** Extraction, indirection, and value-object wrapping can cost in hot loops. If profiling shows it, keep the smell and document why. 5. **Deliberate designs that mimic smells.** Strategy/Visitor look like Feature Envy; DTOs and events look like Data Classes; a facade looks like a Middle Man; test fixtures often carry intentional duplication for readability and isolation. Also note **DAMP over DRY** in tests: explicit, slightly repetitive tests are usually better than clever shared abstractions. 6. **Coincidental duplication.** If two copies encode rules that will evolve independently, unifying them is a net loss — "duplication is far cheaper than the wrong abstraction". 7. **Risk windows.** During an incident, a freeze, or a compliance audit, restructuring adds risk with no delivery benefit. 8. **Insufficient understanding.** Refactoring code whose domain rules you do not yet understand converts a comprehension problem into a correctness problem. ## Anti-patterns in smell programmes - **Metric gates as truth.** Failing a build on a method-length or cyclomatic-complexity threshold produces gaming — code split at meaningless boundaries just to pass. Use thresholds as *conversation triggers* and trend indicators, not gates. False positives are common: linters cannot see intent. - **The big refactoring project.** A separately funded, months-long rewrite/cleanup with no user-visible delivery is the classic failure mode; incremental, story-attached refactoring survives budget pressure. - **Deodorant.** Adding comments, renaming to hide confusion, or wrapping in a class without moving behaviour — the smell remains, now harder to see. - **Refactoring the untested.** Doing it "carefully" without tests, then discovering the regression in production. - **Cleanliness as an end in itself.** The goal is cheaper, safer change; a codebase can be beautiful and still slow to change if the boundaries are wrong. ## Recording the decision When you deliberately keep a smell, make it explicit and time-boxed: a short note near the code or a tracked debt item stating *why* it is acceptable and *what would change the answer* ("if this module enters our top-10 hotspots, revisit"). This distinguishes **deliberate, prudent debt** from oversight — Fowler's technical-debt quadrant — and keeps the choice reviewable when the assumptions expire.

  • How would you justify refactoring work to a sceptical product stakeholder?
    Attach it to delivery rather than requesting a separate budget: show that a specific upcoming feature is expensive because of a specific hotspot, and estimate the story with and without the preparatory refactoring. Support it with evidence — that file's change frequency, its share of recent defects or incidents, time lost to merge conflicts — and keep each step releasable so the work can be stopped at any point without leaving a half-migration.
  • What do you do when a linter flags a smell you believe is correct as written?
    Treat the finding as a prompt, not a verdict: confirm whether the structure is a deliberate design (Strategy, DTO, adapter, DAMP test), then suppress it narrowly at that site with a written reason, or adjust the rule if the false-positive rate is high across the codebase. Blanket-disabling loses the signal; silently ignoring it trains the team to ignore all findings.

Smells are like maintenance items on a car: you service the one you drive daily, not the one on blocks in the garage — and you do not rebuild the engine the morning of a long trip.

saying these in an interview costs you the question

  • Treating every catalogue smell as a defect that must be fixed regardless of context
  • Proposing a large, separately funded cleanup project instead of incremental refactoring attached to delivery
  • Refactoring legacy code without characterisation tests and calling it safe because the steps were small
  • Using complexity or method-length gates as build-failing rules, which invites gaming rather than design improvement
  • Mixing behaviour changes with restructuring in one commit, making review and bisection unreliable
  • Claiming clean code is an end in itself rather than a means to cheaper, safer change

context