---
title: "Refactoring Avoidance"
type: "ai-smell"
slug: "refactoring-avoidance"
url: "http://localhost:3000/en/ai-smells/refactoring-avoidance.md"
category: "Structure"
description: "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."
---
# 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.

```ts
// 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**.

```ts
// 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

- **jscpd** `duplication above --threshold / --min-tokens` — Copy/paste detection (https://github.com/kucherenko/jscpd)
- **PMD** `Copy/Paste Detector (minimum-tokens duplication)` — CPD (https://pmd.github.io/pmd/pmd_userdocs_cpd.html)
- **SonarQube** `duplicated_lines_density / quality-gate duplicated-blocks condition` — Duplications (https://docs.sonarsource.com/sonarqube-server/latest/user-guide/code-metrics/metrics-definition/)
- **ESLint (eslint-plugin-sonarjs)** `sonarjs/no-identical-functions` — no-identical-functions (https://github.com/SonarSource/eslint-plugin-sonarjs/blob/master/docs/rules/no-identical-functions.md)
- **ESLint (eslint-plugin-sonarjs)** `sonarjs/no-duplicate-string` — no-duplicate-string (https://github.com/SonarSource/eslint-plugin-sonarjs/blob/master/docs/rules/no-duplicate-string.md)
