The cost of a clean abstraction
Every abstraction is a bet that the future resembles the present. Here is how to price that bet before you place it.

There is a particular kind of code review comment I have written too many times, and received too many times, and it goes like this: could we pull this into a shared helper? It is almost always well meant. It is often wrong.
The instinct behind it is sound. Two pieces of code look alike, duplication is a known source of bugs, and consolidating them into one place means fixing things once instead of twice. Every programmer learns this early, usually right after being burned by a bug they fixed in three files and missed in a fourth.
But the instinct skips a step. It treats similarity as if it were the same thing as sameness, and those come apart faster than anyone expects.
Similarity is not sameness
Two functions that look identical today can be identical for entirely unrelated
reasons. A formatPrice in the checkout flow and a formatPrice in the admin
reporting view might both put a dollar sign in front of a number rounded to two
places. Merge them and you have made a claim: that these will always agree.
Then the reporting team asks for accounting-style negatives in parentheses, and the checkout team asks for a currency switch, and your one clean helper grows two boolean flags that no caller understands.
The wrong abstraction is more expensive than the duplication it replaced, because duplication is obvious and a bad abstraction is load-bearing.
Sandi Metz, roughly, and she was right
What went wrong was not the merge. It was the missing question: are these the same thing, or do they merely look alike right now?
Pricing the bet
An abstraction is a bet that a set of call sites will change together. That gives you a way to price it. Ask what happens when one caller needs different behaviour, and how much work it takes to get it.
| Change pattern | What it costs you | Verdict |
|---|---|---|
| All callers change together | One edit, one test | Abstract it |
| Callers change independently | Flags, branches, coupling | Leave it duplicated |
| Unclear which | Wait for a third case | Leave it duplicated |
That third row does most of the work in practice. Two examples is not a pattern, it is a coincidence with a witness. The third occurrence is where the real shape shows up, and it is usually not the shape you would have guessed from the first two.
What to do instead of abstracting
The useful move, when you spot duplication you are not ready to consolidate, is to make it findable rather than to make it disappear.
- Name them the same thing. Grep is a perfectly good index, and
two functions called
formatPricein different modules are trivially discoverable without being coupled. - Cross-reference in a comment. Two lines in each file pointing at the other. No import, no shared module, no coupling.
- Let it rot when it should. If they drift apart, the comment becomes wrong and you delete it. A stale comment costs a moment; a wrong abstraction costs a quarter.
// Also implemented in admin/reporting/format.ts — intentionally separate.
// These agreed as of Aug 2026 but serve different audiences, so let them drift.
export function formatPrice(cents: number): string {
return `$${(cents / 100).toFixed(2)}`;
}
That comment is doing real work. It tells the next person the duplication is a decision rather than an oversight, which is the difference between code they will respect and code they will “clean up” on a slow Friday.
The part nobody writes down
Undoing a bad abstraction is harder than making one, and not for technical reasons. Inlining a helper back into six call sites is mechanical. What stops people is that the helper has a name, and the name sounds like a concept, and arguing that a concept in your codebase is fake feels like arguing about philosophy rather than code.
So the asymmetry is real: abstracting is cheap and reversible in theory, expensive and sticky in practice. Price it accordingly. When in doubt, wait. The duplication will still be there next month, and by then you will know more.


