ConstructiCat Logo
CodeBust.
Browse section ▾

Refactoring Avoidance.

AI assistants pile new, near-duplicate code next to what already exists instead of restructuring it, so duplication climbs and refactoring — the rework that keeps a codebase healthy — quietly disappears.

##Signs and Symptoms

A reviewer sees a diff that is almost entirely additions. The model solved the prompt by appending a fresh function, branch, or file rather than editing the abstraction that should have absorbed the change. Tell-tale signs:

  • Copy-paste-with-tweaks: a new function is 90% identical to an existing one, differing by a literal, a field name, or one extra if.
  • Parallel near-duplicates: formatUserCsv, formatAdminCsv, formatGuestCsv all hand-rolling the same loop instead of one parameterized function.
  • Branch sprawl instead of extraction: a long method grows another else if rather than the model pulling a strategy/lookup table out.
  • Reinvention: a hand-written deepClone/debounce/date parser when the repo already imports lodash, date-fns, etc. (OX Security calls this "Vanilla Style".)
  • Over-specification: a hyper-specific single-use helper where a generic one already exists two files over.
  • "Bugs déjà-vu": the same fix has to be applied in three copies because the duplicate was never DRYed up.
// Existing in the repo:
function priceWithTax(items: Item[]) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
  return subtotal * 1.2; // 20% VAT
}

// What the model adds for the new "discounted" case —
// a whole second copy instead of a parameter:
function priceWithTaxDiscounted(items: Item[], discount: number) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0); // duplicated
  return subtotal * (1 - discount) * 1.2;                          // duplicated VAT logic
}

The smell is structural, so it is most visible across the whole change, not one hunk: lots of green, little code moved, and a duplication scanner lighting up on the new lines.

##Reasons for the Problem

Why models produce it

  • Additive next-token bias. An LLM completes the prompt in front of it. Emitting a self-contained new block is the locally highest-probability, lowest-risk continuation; editing a distant abstraction requires holding the whole module in working context and predicting a globally-consistent edit, which is harder and not what the immediate prompt rewards.
  • Thin repo context. Coding assistants rarely load the entire codebase. If the model can't see the existing helper, library, or base class, it can't reuse it — so it rebuilds. OX Security's report frames this as the "Army of Juniors" effect: lots of locally-functional code, no architectural memory.
  • Refactoring is risky and unrewarded. Restructuring touches code the model wasn't asked to change and can break callers and tests. A sycophantic, "just make it work" assistant minimizes blast radius by not touching working code — exactly the behavior OX measured as "Avoidance of Refactors" in 80–90% of AI-generated code, and "Over-Specification" (single-use over reusable) in another 80–90%.
  • Training-data staleness. The model may not know the repo adopted a util module or upgraded a library after its cutoff, so it hand-rolls what already exists.
  • Generation is cheap, deletion is scary. Producing 40 new lines costs the model nothing; convincing it to delete and consolidate 40 existing lines fights its instinct to preserve.

Why it hurts

  • Duplication compounds. GitClear's 2025 analysis of 211M changed lines found copy/pasted lines rose from ~8.3% (2021) to 12.3% (2024) — the first year duplication exceeded "moved" (refactored) code — while refactored lines fell from ~25% to under 10%, roughly a 60% drop. Blocks of 5+ duplicated lines jumped ~8x in 2024.
  • Maintainability and correctness. Every clone is a place a future fix can be forgotten — "Bugs déjà-vu," where the same defect recurs and must be patched N times (OX: 70–80% of AI code violates reuse principles this way).
  • Tech-debt accrual. Refactoring is the rework that keeps entropy down; suppressing it means debt is created but never repaid. The codebase grows faster than it improves.
  • Review load. Reviewers must now diff near-identical blocks by eye to confirm they're intentionally the same, the cognitive-load shift Stack Overflow's "code smells for AI agents" discussion describes — the work moves from writing to reviewing and consolidating.

##Treatment

Prompting / review tactics

  • Point the model at what exists. "Before adding code, search the repo for an existing helper/util/base class and reuse it; if none fits, generalize the closest one." Paste the relevant module into context so it can actually see the abstraction.
  • Constrain the diff shape. "Prefer editing existing functions over adding new ones. If two code paths share logic, extract a shared function (Extract Function) rather than duplicating."
  • Make it run the tools. Require the assistant to run the duplication scanner (jscpd / PMD CPD) and the linter, and to resolve any new duplicated-block findings before declaring done — this turns an invisible smell into a failing gate.
  • Ask for the refactor explicitly as a second step. Generation and consolidation are different tasks; do "make it work," then a separate "now DRY this up and remove duplication," which models handle far better when asked directly.
  • Add a quality gate in CI so duplication can't ratchet up unnoticed (Sonar duplicated-lines threshold, or jscpd --threshold).

The actual refactor — name the classic moves: Extract Function, Parameterize Function, and Pull Up / Replace Conditional with Polymorphism or a lookup table to kill Duplicate Code.

// Before: two near-identical functions (refactoring avoided)
function priceWithTax(items: Item[]) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
  return subtotal * 1.2;
}
function priceWithTaxDiscounted(items: Item[], discount: number) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
  return subtotal * (1 - discount) * 1.2;
}

// After: Extract Function + Parameterize Function
const VAT = 1.2;
const subtotalOf = (items: Item[]) =>
  items.reduce((s, i) => s + i.price * i.qty, 0);

function priceWithTax(items: Item[], discount = 0) {
  return subtotalOf(items) * (1 - discount) * VAT;
}

One source of truth for subtotal and VAT: a future change to the tax rule is now a one-line edit instead of an N-place hunt.

##Detected by