Your codebase has twenty ways to format money
6m read time

Your codebase has twenty ways to format money

AI code duplication is quiet: an inherited codebase with twenty inconsistent money and date formatters. How near-clones form, how to spot them, and the CLAUDE.md rules that stop them.

Yesterday I was working in a codebase that wasn't mine. A handoff from another developer who had, to put it kindly, vibecoded the whole thing: agent at the wheel, nobody reading the diffs. I needed to change one function, a single place that turned a number into money. I went looking for it and found twenty.

formatMoney. toCurrency. displayPrice. formatAmount. priceLabel. Some rounded half-up, some just truncated. Two hard-coded a euro sign, one guessed the currency from the locale, one put the symbol after the amount, one before. Dates were worse: I stopped counting at a dozen. formatDate, humanDate, dateStr, toDisplayDate, each with its own idea of which timezone you meant.

Nobody wrote these on purpose. Nobody sat down and decided the codebase needed a fifth way to print a price. They accumulated, one session at a time, one prompt at a time, each one reasonable in the five lines around it and indefensible the moment you saw all twenty together.

This is what AI does to DRY. Not with a bang. With amnesia.

The agent starts cold every time

An agent does not know your codebase. It knows the file you handed it and whatever fits in the window. Ask it to display a price and it will write you a competent little formatter, because writing one is easy and checking whether the repo already has one is work it was never asked to do.

It has no memory of the formatter it produced three prompts ago that same afternoon. It has no world model of the repo. Every request arrives on a blank slate, and a blank slate solves every problem for the first time.

A human does this too, occasionally. The difference is a human feels the cost. Writing the function is typing, and typing is friction, and friction makes you pause and think "surely we have this already". The agent feels nothing. It produces the twenty-first formatter as happily as the first, in the time it takes you to read this sentence.

Copy-paste was honest by comparison

We used to worry about copy-paste engineering. That fear looks quaint now.

When a human copy-pastes, they duplicate a known function. There is an original. It has a name you can grep for. When it turns out to be wrong, you fix the one source, and the copies are at least findable because they look identical.

Where a human copies, an agent generates. Every clone is novel: same job, different name, different edge cases, different bugs. You cannot grep for formatMoney and find the other nineteen, because they are called toCurrency and renderPrice and amountString. None of them is the original, because none of them copied one.

Now count the rounding. Four of those formatters rounded differently. Four different numbers on four different screens for the same order. A support ticket nobody will reproduce, because it depends on which formatter rendered which page.

Duplication used to be a maintenance problem. AI duplication is a correctness problem wearing a maintenance problem's clothes.

You will not notice until you go looking

Here is the trap: none of this shows up while you read the diff.

Each PR adds one small, sensible helper. The diff is green. The tests pass, because the new formatter is tested against itself. The reviewer sees five clean lines and approves. The twentieth formatter enters the codebase exactly as smoothly as the first, which is precisely the rot that slips past PR review when every individual change looks fine.

You find it the way I found it: by needing to change how money is displayed, and discovering the job is spread across twenty helpers, and the archaeology is now yours.

This is the quieter cousin of measurable code churn. Churn you can put a number on. Near-clones hide, because every one of them is technically live code that technically works. It is the lava layer forming one helper at a time.

Spotting the near-clones

  • Grep by verb, not by name. You will not find duplicates by their names, because their names are the whole problem. Search for what they do: Intl.NumberFormat, toFixed, .getTime(), currency symbols, date-library imports. The call sites cluster even when the function names scatter.
  • Run a clone detector. Tools like jscpd, and language-specific similarity checkers, find structural near-duplicates that no grep will, precisely because they ignore the names. Run one over a vibecoded codebase and the count is usually higher than you would guess.
  • Watch the import spread. Five files importing date-fns, three importing dayjs, two hand-rolling with Date: that is the date logic telling you, in the import list, for free, that it fragmented.

The fix is memory, not discipline

You cannot discipline your way out of this, because the thing generating the duplicates runs on context alone. So you fix it where that context lives.

This is what CLAUDE.md is for: a short, enforced set of "we already have this, use it" pointers, the kind a style guide never manages to be.

markdown
## Formatting: there is exactly one of each

- Money: `formatMoney(cents, currency)` in `src/lib/money.ts`. Do NOT
  write a new currency formatter. If it can't do what you need, extend it.
- Dates: `formatDate(date, style)` in `src/lib/date.ts`. All display dates
  go through it. Never call `toLocaleDateString` directly.

Before adding any `format*` / `to*` / `display*` helper: grep `src/lib`
for an existing one first.

That last line is the important one. You are handing the agent the search it would never run on its own. Conventions the agent cannot see may as well not exist, which is the whole reason a missing or stale convention costs you more than you think.

It will not be perfect. It will still occasionally reach for a fresh formatter, and you will still catch some in review. But the default flips from "invent" to "check first", and the twenty stops becoming twenty-one every sprint.

DRY was always about having one place in the codebase to be right. Typing less was a side effect. One place, so that when the rounding rule changes, or the currency does, or the timezone bug finally gets reported, you change it once and you are done.

AI made producing code cheap, and producing duplicate code just as cheap. The one place to be right quietly became twenty places to be subtly wrong. Those twenty formatters were not mine, and that is the point: the developer who generated them never saw all twenty either. The agent produced each one in isolation, for the first time, and moved on. The bill lands on whoever has to work in it. Yesterday that was me.

The codebase remembers nothing on its own. Neither does the agent. That job belongs to whoever is still around to care.