Code Quality

The Code Smells Every Audit Flags

A smell is a symptom, not a verdict

“Code smell” is one of the most useful terms Kent Beck and Martin Fowler ever coined, and one of the most misused. A smell is not a bug. It’s a surface signal that the design underneath is under stress, the way a hot laptop fan doesn’t mean anything is broken, but tells you something is working harder than it should.

That framing matters for audits, because it sets the bar for flagging. We don’t flag every long function; we flag the smells that, in our experience, reliably predict the two things that cost companies money: defects and slow delivery. Here are the ones that clear that bar, ordered roughly by how often they turn out to matter.

The heavy hitters

The God object

One class or module that knows too much and does too much: the 3,000-line UserService that handles auth, billing, notifications, permissions, and CSV export. You can find it without reading code: it’s the file with the most commits in git log, because every feature touches it. That’s the actual problem: a God object turns the whole team into a queue. Merge conflicts concentrate there, every change risks unrelated breakage, and it accretes because adding to it is always locally cheaper than splitting it.

Audit note: God objects and authorization bugs travel together. When one class does everything, permission checks end up scattered through it inconsistently, which is how the missing-check vulnerabilities get in.

Duplicated logic (the divergent kind)

Copy-paste itself is survivable. The dangerous variant is divergent duplication: the same business rule implemented in three places, where a fix landed in two of them. Price calculation done in the API and re-done slightly differently in the report generator; email validation with three different regexes. Every audit finds at least one rule whose implementations disagree, at which point the question “which one is correct?” often has no answer anyone can give.

This smell has a modern accelerant: AI coding assistants, which cheerfully regenerate similar-but-not-identical logic instead of reusing what exists. We’ve written about that pattern in hidden debt in AI-generated code.

Shotgun surgery

The inverse test of good structure: pick a plausible small change (“add a field to the invoice”) and count the files it touches. If the answer is eleven, across three layers, plus two config files and a test fixture that duplicates the schema, the design has smeared one concept across the codebase. Shotgun surgery is the smell that best predicts delivery pain: it’s why “small” tickets take a week, and it compounds into estimates nobody believes. It’s also the mechanical definition of a big slice of technical debt.

Dead code

Commented-out blocks from 2024, feature-flagged paths for flags that will never flip again, the v1/ directory kept “just in case,” functions with zero callers. Teams underrate this one. Dead code costs reading time on every pass, shows up in searches, still gets “fixed” during refactors; and, the audit-specific concern, dead code still executes if it’s reachable: old endpoints that were superseded but never removed are unmaintained attack surface that nobody is watching. Delete it; git remembers.

Silent failure

try {
  await syncToWarehouse(order);
} catch (e) {
  // TODO: handle this
}

The empty catch block is arguably a bug already in progress rather than a smell, but it’s caught by reading, not by tests: nothing fails, that’s the point. Every codebase has a few; the audit finding is when they sit on paths that matter (payments, data sync, permission loading; an auth check that fails open because its errors are swallowed is a security hole wearing a quality costume).

The medium tier

These get flagged when they cluster or sit on critical paths:

  • Long parameter lists and boolean flags. createUser(name, email, true, false, null, true): six months later, nobody can read a call site without opening the definition. Flags mean one function is two functions.
  • Deep nesting. Arrow-shaped code five conditionals deep. Beyond readability, the audit concern is that deeply nested branches are where untested paths hide: the sad-path handling at nesting level 4 has usually never executed outside production.
  • Feature envy. Methods that spend all their time reaching into another object’s data (order.customer.address.country.taxRate): logic living in the wrong home, and a coupling chain where a change to Address breaks Order code.
  • Primitive obsession. Money as floats (a genuine correctness bug; ask anyone who’s reconciled 0.1 + 0.2), user IDs and order IDs both bare ints so they can be swapped silently, statuses as magic strings with no enum.
  • Speculative generality. Abstract factories with one concrete implementation, plugin systems with no second plugin, AbstractBaseHandlerStrategy hierarchies built for a future that never arrived. Flexibility nobody asked for is complexity everybody pays for.

What we deliberately don’t flag

Half the value of a good audit is the noise it doesn’t generate. Things that look like smells but usually aren’t worth a finding: long-but-linear functions (a 90-line function that reads top-to-bottom like a recipe is often clearer than nine 10-line fragments you have to chase), TODO comments (honest markers, fine), an ugly-but-isolated module with stable behavior and no change pressure (leave it alone; refactoring it is risk with no payoff), and style-guide deviations (that’s a linter’s job, and linters work for free).

The ranking rule underneath all of this: smell severity = intrinsic badness × how often the code changes × what breaks if it’s wrong. A God object in your billing core is a top finding. The same object in an internal tool nobody has touched since 2024 is a footnote.

Finding them in your own repo

A rough self-audit takes an afternoon:

  • git log --format=format: --name-only | sort | uniq -c | sort -rg | head -20: your most-changed files. God objects and shotgun-surgery hubs live at the top.
  • Run a duplication detector (jscpd, PMD’s CPD) and look for logic duplication, not just boilerplate.
  • Grep for empty catches / bare excepts and read each one asking “what breaks silently here?”
  • Pick one recent “small” ticket and count the files its PR touched.

Get a free code audit

Or have someone with no attachment to the code do it. Our audit is genuinely free: a Webisoft engineer reads your repository by hand and sends a written report covering the smells that actually predict incidents in your codebase, alongside security findings, all ranked by risk-times-change-frequency rather than raw count. If you suspect you know which file is your God object but want it confirmed by someone who can’t hurt its feelings, get a free code audit.

FAQ

Frequently asked questions

Is a code smell the same thing as a bug?

No. A smell is a surface signal that the design underneath is under stress, the way a hot laptop fan tells you something is working harder than it should. Some smells sit right next to bugs (an empty catch block is arguably a bug in progress), but most cost you through defect risk and slow delivery rather than immediate breakage.

How do I find the God object in my own codebase?

You do not even need to read code: sort your files by commit count in git log. God objects sit at the top because every feature touches them, which is also the real cost: they turn the whole team into a queue and concentrate merge conflicts in one file.

Which smells are actually worth fixing first?

Rank by intrinsic badness, times how often the code changes, times what breaks if it is wrong. A God object in your billing core is a top finding; the same mess in an internal tool nobody has touched in years is a footnote. Divergent duplication, shotgun surgery, and silent failure on critical paths are the ones that most reliably predict incidents.

Should I refactor an ugly module that works and never changes?

Usually no. An ugly but isolated module with stable behavior and no change pressure is debt at zero interest, and refactoring it is risk with no payoff. Spend that effort on code that is both complicated and frequently modified instead.

Why is dead code a security concern rather than just clutter?

Because dead code still executes if it is reachable. Old endpoints that were superseded but never removed are unmaintained attack surface that nobody is watching, on top of the reading tax every pass through the file pays. Delete it; git remembers.

Free · no strings shown later

Get your free code audit

A repo URL and two minutes of your time. A Webisoft engineer sends back written findings (security, tech debt, performance) with file-and-line specifics.

Request my free audit