DRY Is About Knowledge, Not Code
The rule says every piece of knowledge must have a single authoritative representation. It says nothing about text. Two functions that look alike but encode different rules are not a violation — and merging them creates a coupling that did not exist.
Two functions, eleven lines each, near-identical. The reviewer writes "DRY?" and everyone nods.
Six months later that helper takes four boolean parameters, has a branch for every caller, and carries a comment explaining which flags go together. Nobody touches it without reading all of it. The duplication is gone and the code is worse.
The rule was applied correctly. The rule was just never about text.
What the rule actually says
The canonical wording is worth reading slowly, because almost nobody does:
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
Knowledge. Not lines, not tokens, not shape.
And note the scope: within a system, not within a codebase. The rule bites in places that have nothing to do with code — a database schema, a deployment script, a spreadsheet the finance team maintains, a paragraph of documentation that restates a business rule. It is a claim about where a decision lives, and code is only one of the places decisions live.
Read that way, the reason it matters is mechanical rather than aesthetic. One idea expressed in N places means N edits and N chances to miss one. The question was never whether you would remember the second copy. It is when you will forget.
That is the cost DRY exists to prevent: the error of omission. Not ugliness. Not repetition. The bug you ship when you change the tax rule in three places and there was a fourth.
The test
One question, and it is answerable in review:
If this rule changed, would both places have to change?
Yes → one piece of knowledge in two locations. Real duplication. Deduplicate it.
No → two pieces of knowledge that currently look alike. Coincidence. Leave them.
Consider:
// Validates a US zip code.
function isValidZip(s: string): boolean {
return /^\d{5}$/.test(s);
}
// Validates a product SKU, which today is also five digits.
function isValidSku(s: string): boolean {
return /^\d{5}$/.test(s);
}
Identical bodies. Merge them into isFiveDigits and you have asserted that zip codes and SKUs are the same concept. The day SKUs gain a letter prefix — and they will — you discover the coupling you created for free, and the fix is to undo a "refactor" that was praised.
Now the other case:
// checkout.ts
const total = subtotal * 1.21;
// invoice.ts
const total = subtotal * 1.21;
These look less similar in context and are far more dangerous. That is one piece of knowledge — the VAT rate — in two places. When it changes to 1.22, one of them gets missed.
The lesson generalises: textual similarity is a bad predictor of shared knowledge, in both directions.
The cost of getting it backwards
Deduplicating two independent rules couples them permanently. They now change together whether or not the business intends it, which is the definition of the coupling you spend your career trying to reduce — except you introduced it deliberately, and a review approved it.
The decay is predictable. First divergence adds a parameter. Second adds a boolean flag. Third adds a branch on the flag. By the fourth, the "shared" function is a switch statement over its callers, and its name — processItem — describes nothing, because it does whatever the flags say.
Meanwhile the thing DRY protects, orthogonality, has been quietly destroyed: unrelated concerns can no longer change independently.
Sandi Metz put the rule of thumb better than anyone: duplication is far cheaper than the wrong abstraction. Duplication is a debt you can pay off later, once you know which parts genuinely vary. The wrong abstraction is one you pay forever, and paying it off means unpicking every caller.
The four causes, because the fix differs
Duplication arises in four distinct ways, and knowing which one you are looking at is more useful than "don't do it":
- Imposed — the environment seems to force it: an interface and its implementation, docs mirroring code, the same model expressed on three platforms. Usually beatable with a generator or a single source of truth. Fix the mechanism.
- Inadvertent — you did not realise, like a
lengthfield duplicatingstartandend. Make it calculated, not stored. - Impatient — copy and tweak, under deadline, promising yourself you will come back. This is the only one of the four that is actually about discipline.
- Interdeveloper — different people or teams independently re-implement the same thing. The hardest kind by far: grep a large enterprise codebase for national-ID or postcode validation and you will find dozens of implementations, each written by someone who checked nothing. This is a communication problem, not a code problem.
Notice that only one of the four is solved by noticing duplication in a diff. The most expensive kind — interdeveloper — is invisible in code review by construction, because the two copies are in different repositories owned by different teams.
What to say in review
Replace "that's not DRY" with a question:
Is that the same rule?
It is answerable, it is about the domain rather than the diff, and it produces a real conversation. Sometimes the answer is "no, they're both five digits by coincidence" and you move on. Sometimes it is "actually yes, and there's a third copy in the reporting job" — which is the outcome you wanted, and which counting duplicated lines would never have surfaced.
And when you genuinely do not know yet, leave it duplicated. Waiting costs you a second copy. Guessing wrong costs you an abstraction everyone has to route around.
DRY is a rule about knowledge. If the two snippets do not encode the same decision, they are not duplicates — they just rhyme.