DRY Is About Knowledge, Not Code
The most misquoted rule in software is DRY.
Most people learn it as: never write the same lines twice. So they extract a helper the moment two functions rhyme, and end up with a shared abstraction that two callers now have to fight over.
The original phrasing is sharper: every piece of knowledge must have a single, unambiguous, authoritative representation within a system. The unit is knowledge, not characters.
Two functions that look identical
function formatInvoiceTotal(cents: number) {
return `$${(cents / 100).toFixed(2)}`;
}
function formatCartTotal(cents: number) {
return `$${(cents / 100).toFixed(2)}`;
}
Identical bodies. Extracting formatMoney feels obvious.
But ask what knowledge each encodes. One is how finance renders a billed amount on a legal document. The other is how the storefront renders a running total. Those are two decisions that happen to agree today. The first time invoices need a currency code and the cart doesn't, the shared helper grows a boolean, then a second boolean, and the abstraction starts lying about what it is.
The inverse case
The more expensive duplication is invisible:
// checkout.ts
if (user.plan === "pro" || user.seats > 5) { ... }
// billing.ts
const isEnterprise = user.seats > 5 || user.plan === "pro";
// emails.ts
if (user.seats >= 6 || user.plan !== "free") { ... }
Three spellings of one business rule, and the third already disagrees. No linter flags this, because no two lines match. This is the duplication DRY was written about.
The test I use now
Before extracting, I ask one question: if this rule changes, do all callers have to change together?
- Yes, always, by definition of the rule -> one representation.
- No, they only agree by coincidence -> leave them alone.
That question is cheap and it front-loads the decision that a shared helper would otherwise defer until the code is expensive to untangle.
Duplicated code is a smell. Duplicated knowledge is a bug waiting for a deadline.