# Duplicate Code

> The same logic appears in two or more places, so every fix and change must be made several times and the copies drift apart.

- kind: smell
- category: dispensables
- aliases: Clones, DRY violation
- canonical: https://aipatterns.dev/refactoring/smells/duplicate-code
- inspired-by: https://refactoring.guru/smells/duplicate-code

## Signs

- Two functions are identical except for a name, a constant or a field.
- A bug fix in one place needs to be repeated in another, and sometimes was not.
- Sibling subclasses contain the same method body.
- Two methods compute the same result by different algorithms.
- Both branches of an `if` end with the same lines.
- Two components render near-identical markup, and only the shared data was deduplicated.

## Why it happens

Copying working code is the fastest way to ship a similar feature. Different people solve the same problem without knowing the other solution exists. Deadlines discourage the extra hour it takes to find a shared shape. And sometimes code looks the same by coincidence today and is supposed to diverge tomorrow; that is not duplication.

## Example

Two report exporters in an analytics service share the same date filtering and totaling logic.

```ts title="exports.ts"
interface Sale { at: Date; region: string; cents: number }

function exportCsv(sales: Sale[], from: Date, to: Date): string {
  const inRange = sales.filter((s) => s.at >= from && s.at < to);
  const total = inRange.reduce((sum, s) => sum + s.cents, 0);
  const rows = inRange.map((s) => `${s.at.toISOString()},${s.region},${s.cents}`);
  return ["at,region,cents", ...rows, `total,,${total}`].join("\n");
}

function exportJson(sales: Sale[], from: Date, to: Date): string {
  const inRange = sales.filter((s) => s.at >= from && s.at <= to); // drifted: <= vs <
  const total = inRange.reduce((sum, s) => sum + s.cents, 0);
  return JSON.stringify({ rows: inRange, total });
}
```

The copies already disagree on the end boundary. After [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md):

```ts title="exports.ts"
interface Sale { at: Date; region: string; cents: number }

function salesInRange(sales: Sale[], from: Date, to: Date) {
  const rows = sales.filter((s) => s.at >= from && s.at < to); // half-open, one rule
  const total = rows.reduce((sum, s) => sum + s.cents, 0);
  return { rows, total };
}

function exportCsv(sales: Sale[], from: Date, to: Date): string {
  const { rows, total } = salesInRange(sales, from, to);
  const lines = rows.map((s) => `${s.at.toISOString()},${s.region},${s.cents}`);
  return ["at,region,cents", ...lines, `total,,${total}`].join("\n");
}

function exportJson(sales: Sale[], from: Date, to: Date): string {
  return JSON.stringify(salesInRange(sales, from, to));
}
```

Decide which boundary is correct before merging. Merging silently changes behavior for one caller.

## Treatment

- If the copies are in the same class, use [Extract Method](https://aipatterns.dev/refactoring/techniques/extract-method.md) and call it from both places.
- If the copies are in sibling subclasses, extract, then use [Pull Up Method](https://aipatterns.dev/refactoring/techniques/pull-up-method.md).
- If the siblings share the same steps but differ in details, use [Form Template Method](https://aipatterns.dev/refactoring/techniques/form-template-method.md).
- If two methods do the same thing with different algorithms, pick the clearer one with [Substitute Algorithm](https://aipatterns.dev/refactoring/techniques/substitute-algorithm.md).
- If the copies live in unrelated classes, use [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md) when they share an "is a" relationship, or [Extract Class](https://aipatterns.dev/refactoring/techniques/extract-class.md) when they do not.
- If several conditions lead to the same action, use [Consolidate Conditional Expression](https://aipatterns.dev/refactoring/techniques/consolidate-conditional-expression.md). If every branch ends with the same code, use [Consolidate Duplicate Conditional Fragments](https://aipatterns.dev/refactoring/techniques/consolidate-duplicate-conditional-fragments.md).
- If the copies are UI markup, extract one shared component. Sharing the data (an options list, a constant) is not enough: the copy-pasted tags and handlers stay until one component owns them.

## Payoff

- One place to fix bugs and change rules.
- Less code to read, test and maintain.
- Hidden drift between copies surfaces and gets decided once.

## When to ignore

- The code looks alike but represents different concepts that will change for different reasons. Merging couples them; wait for a third copy to confirm the pattern.
- The copies are in separate services or packages, and sharing would add a dependency that costs more than the duplication.
- Test code, where explicit repetition often reads better than a clever shared helper.
- Two or three trivial lines, where the helper would be harder to read than the copy.

## Agent checklist

- [ ] You compared every copy line by line and listed the differences before merging.
- [ ] Each difference was either confirmed as a bug and fixed, or kept as a parameter.
- [ ] The copies change for the same reason; otherwise you left them separate.
- [ ] You checked for markup duplication, not only logic duplication.
- [ ] You did not add a cross-package dependency to remove a few lines.
- [ ] Tests for every former caller pass, including a case on each boundary that differed.
