DRY: Knowledge, Not Lines
The rule is about a piece of knowledge having one authoritative home. It is routinely remembered as a rule about text, and that misreading produces shared abstractions that couple things which have nothing to do with each other.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
Two pieces of code look alike. How do I tell whether they are one thing written twice, or two things that happen to resemble each other today?
A reviewer blocks a pull request: "this validation is duplicated, extract it". The two blocks are eleven identical lines, in the signup flow and in the supplier import.
Identical code is duplication. Extract it to a shared helper, call it from both, and the codebase gets smaller. That is what the rule says.
The rule as usually recited is about text, and text similarity is not evidence of anything. The eleven lines encode two different decisions — an account policy and a partner contract — that agree today by coincidence.
- The rule as usually recited is about text, and text similarity is not evidence of anything. The eleven lines encode two different decisions — an account policy and a partner contract — that agree today by coincidence.
- Six months later the partner contract changes a length limit. The engineer edits the shared helper, the tests pass, and signup validation changes with it. Nobody was testing that interaction because nobody knew it existed.
- The usual repair makes it worse: a boolean or a mode parameter is added to the shared function so the two callers can differ, and the function becomes a place where two unrelated policies are interleaved (Boolean Parameters).
- Meanwhile the genuine duplication in this codebase is invisible to the same rule, because it does not look alike: the discount eligibility rule exists as a SQL
WHEREclause in a report, anifin the checkout service and a line in a marketing email template.
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- Both blocks are in the same repository and both currently pass the same tests.
- The signup rules come from the account team; the supplier import rules come from a partner contract.
- The reviewer is right that the code is identical, and is senior enough that "no" needs an argument.
- Whatever the outcome, a rule that must hold in both places must be impossible to change in one place only.
- A rule that must hold in only one place must be changeable without touching the other.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Each piece of knowledge — one rule, one format, one decision — has exactly one module responsible for stating it.
- Everyone else is responsible for asking that module, not for restating the rule in a shape convenient to them.
- A shared helper is responsible to all of its callers simultaneously, which is the obligation people take on without noticing when they extract for textual reasons (Shared Libraries).
- The boundary is around the knowledge, not around the syntax. "Account password policy" is a boundary; "string validation" is a folder (The Utility Dumping Ground).
- Two rules with different owners belong on different sides of a boundary even when their current implementations are byte-identical, because ownership predicts divergence and text does not.
- When knowledge genuinely is shared, the boundary should make bypassing it awkward — a rule you can reimplement without noticing will be reimplemented (Enforcing Invariants).
The same knowledge does not have to look the same
The version of this rule that survives contact with a real codebase is about a decision having one authoritative home. The version usually recited is about text, and the two disagree in both directions: identical code that is two decisions, and utterly different code that is one.
The second kind is where the money is, and it is invisible to any tool that finds duplicate blocks. One threshold, three languages, three teams, and the only thing connecting them is that a product manager once said "orders over 500".
1report/monthly.sql2 WHERE total_cents >= 50000 AND status = 'paid'3 4checkout/discount.ts5 if (order.totalCents >= 50_000 && order.isPaid) applyFreeShipping()6 7email/templates/receipt.hbs8 {{#if (gte total 500)}}Your order qualified for free shipping{{/if}}9 10db/migrations/0142_add_check.sql11 CHECK (free_shipping = (total_cents >= 50000))Four statements of one rule, in four languages, with four owners. When finance moves the threshold to 400, three of these get changed and the fourth is found by a customer. No duplicate-code detector flags any of it, and the eleven identical validation lines that reviewers do argue about cost nothing by comparison (Duplicate Knowledge).
What a shared helper actually signs up for
Extraction is usually discussed as though it only removes something. It also creates an obligation: the extracted unit is now answerable to every caller at once, and each new caller adds a reason it might have to change.
Writing that obligation out is the fastest way to see whether an extraction is a good idea. A unit with one reason to change is a rule; a unit with four unrelated reasons is two rules that got married (Single Responsibility, Carefully).
- — The account team's minimum and maximum username length
- — The partner contract's field length limits for supplier names
- — That one caller allows unicode and two do not
- — That the import path needs a lenient mode which returns warnings rather than throwing
- — Validates length, character set and emptiness
- — Returns either a boolean or a warning list depending on a flag
- — Trims, but only when a second flag is set, because one caller depended on not trimming
- — A configuration constant that three modules import
- — Nothing else — which is why it looked so safe to extract
- — The account team changes password or username policy
- — The partner contract is renegotiated
- — A new import source needs a slightly different character set
- — Somebody discovers the trim flag was doing something they relied on
Four unrelated reasons to change, three of which come from different owners, and every change is a change to a module three flows depend on. This is what "the code was identical" bought. The repair is not another parameter — it is two functions, each owned by the team whose policy it states, and eleven lines of duplication that were never the problem (Single Responsibility, Critically).
Three honest responses to code that looks alike
Once the divergence test has been applied, there are three defensible outcomes and one common indefensible one. The indefensible one is merging with a mode parameter, which delivers the coupling without the benefit.
The third option is the one people forget. When the shared thing is a value rather than a behaviour — a threshold, a rate table, a list of country codes — publishing it as data that everyone reads unifies the knowledge without unifying the control flow, and control flow is where shared functions go wrong.
When this decision changes, must every copy change with it?
when A tax rule, a price calculation, an eligibility threshold. One team decides it and it is wrong for two answers to exist.
cost Every caller now depends on one module, which becomes a coordination point and a place that must not break. Delete the old copies in the same change or you have made it worse (Extract Module).
when Identical code, different owners: an account policy and a partner contract that agree today.
cost A genuine shared change would have to be made twice, and someone might miss one. A one-line comment at each site naming the other is usually enough to make that unlikely (Comments).
when The common part is a number, a table or a list, and the surrounding logic genuinely differs per caller.
cost You lose compile-time coupling in most languages, so a stale reader is a runtime problem. In exchange the rule has one home and no caller inherits anyone else's control flow (Choosing the Model).
when Two cases, and you cannot tell which axis varies.
cost You carry the duplication until a third case arrives and shows you the shape. This is the cheap option precisely because duplication is visible and reversible (The Rule of Three).
when Never a good outcome, and extremely common.
cost You take on the coupling of a shared module and the complexity of a branching one, and every future caller adds a mode. If the two sides need flags to coexist, they were two things (Boolean Parameters).
How to build it
Most important first.
- Apply the divergence test before extracting: when this decision changes, must both places change together? Yes means one piece of knowledge; no means two things that look alike (Duplicate Knowledge).
- Ask who owns each site. Two different owners — the account team and a partner contract — is strong evidence of two decisions, and owners diverge long before code does.
- Look for duplicated knowledge in different costumes, which is where the real cost is. Grep for the *number* and the *concept*, not for the code: a threshold that appears in a query, a service and a template is one rule in three places (Shotgun Surgery).
- Prefer sharing data over sharing code when the rule is a value. Publishing the tax rates as data that three modules read is often better than a shared function three modules call, because data has no control flow to diverge (Choosing the Model).
- When you are unsure, duplicate and wait. Duplication is visible, greppable and cheap to unify later; a wrong abstraction is invisible and expensive to unpick, so the asymmetry favours waiting (The Rule of Three).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Two rules kept separate: a change to the partner contract costs one edit and one test file, with no possibility of touching signup. The next change to either is independent, permanently.
- Two rules merged: the same change costs one edit, one test file, and a full understanding of every other caller — plus the risk that the understanding is wrong. That cost grows with each new caller and is paid on every change, not once.
- Genuine knowledge unified: the discount rule moving from three costumes to one owner takes a change from three edits in three languages, plus the discovery cost of finding the third, down to one edit and one test.
- The asymmetry is the point: unifying later costs a refactor; un-unifying later costs a refactor *plus* unpicking behaviour that has since come to depend on the accidental coupling.
- Deliberate duplication means a genuine rule change can be missed in one of the copies, and no amount of discipline makes that risk zero.
- The divergence test requires knowing who owns each rule, which is knowledge some codebases simply do not have written down anywhere (Code Ownership).
- Sharing data instead of code trades a compile-time guarantee for a runtime one in most languages, which is a real loss of safety in exchange for the flexibility.
What can go wrong
- The wrong abstraction: two rules merged, then parameterised, then a third caller arrives, and the shared function accumulates modes until nobody can say what it guarantees (Premature Abstraction).
- The over-correction: a team burned by a bad extraction refuses all sharing, and the discount rule ends up in nine places with three of them stale (Duplicate Knowledge).
- The knowledge is unified in code and duplicated in the database — a check constraint and a validator stating the same rule, drifting apart silently (Invariant Leaks).
- The unification happens, the old copies stay, and now the rule has one authoritative home and four unauthorised ones that still run.
- Extracting creates a dependency from every caller onto the shared thing, and from the shared thing onto the union of everyone's requirements. The second direction is the one that surprises people.
- A shared module with many dependents becomes a coordination point: changing it requires knowing every caller, which is a cost that grows with adoption (Fan-in and Fan-out).
- Leaving them separate creates a dependency on human diligence: someone must notice if they later become one rule. That is a real dependency and the argument against blanket duplication (Review as Design Feedback — and Why It Arrives Too Late).
- "Any repeated text is a violation." Two functions that both check a string is non-empty are eleven identical lines and two decisions. Textual similarity is not the input to the rule (Premature Abstraction).
- "So duplication is fine." Duplicated *knowledge* is one of the most reliable predictors of expensive change there is. The correction is about which duplication counts, not about tolerating all of it (Duplicate Knowledge).
- "WET means write everything twice as policy." The useful version of WET is much narrower: prefer duplicating until you have seen enough cases to know what actually varies. As a blanket policy it produces the nine-copies problem (The Rule of Three).
- "A shared utils module satisfies DRY." A folder named for a part of speech collects unrelated knowledge under one dependency and is the standard way this rule produces harm (The Utility Dumping Ground).
- duplicate-knowledge
- utility-dumping-ground
- shotgun-surgery
Testing it, and how it ages
- Test the rule where it lives, once. If a rule has one home, it has one test file, and finding two test files asserting the same threshold is a reliable signal that the knowledge is duplicated (Duplicate Knowledge).
- For rules that must also hold in the database or on a client, a contract test that asserts both statements of the rule agree — otherwise the drift is only detectable in production (Contract Tests).
- After extracting, test both original call sites still behave as before. A merge is a behaviour-preserving refactor and deserves the same scepticism as any other (What Refactoring Actually Is).
- Rules that started identical drift as their owners diverge, and the drift is a healthy signal — it is how you learn the extraction was wrong, provided the code did not force them to stay equal.
- Knowledge duplication grows silently with the number of technologies in the stack: the same rule ends up in TypeScript, in SQL, in a validation schema and in a form, one per layer (Invariant Leaks).
- The unification eventually gets its own structure — one rule becomes a policy module with several rules — and that is the healthy path, distinguishable from a wrong abstraction by whether the callers are still asking one question (Extract Module).
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThe knowledge framing comes from the original formulation — every piece of knowledge having a single authoritative representation — and applies to code, schemas, configuration and documentation alike, though the enforcement mechanism differs completely between them.
- LANGUAGE-SPECIFICIn a language with strong types and cheap newtypes, unified knowledge can be enforced at compile time — a
Passwordtype that cannot be constructed without passing the policy. In a dynamic language the same design needs a runtime guard and a test to hold, so deliberate duplication is relatively more attractive there because the alternative is weaker (Making Illegal States Unrepresentable). - CONTESTEDThe strongest opposing view: a wrong abstraction is more expensive than any amount of duplication, so the default should be to duplicate freely and only unify under overwhelming evidence — because the cost of un-picking a shared thing that many callers now depend on is far higher than the cost of updating four copies, and unlike a bad abstraction, duplication cannot silently change behaviour somewhere you were not looking. That case is strong and this lesson concedes most of it; where it goes too far is when it is applied to rules with a single owner and a legal or financial definition, where four copies is not a maintenance cost but a correctness bug waiting for the copy someone forgot (Invariant Leaks).
- SIMPLIFIEDThe divergence test is presented as a yes/no question. Real cases include rules that must agree *for now* and are known to be separating later — a partner contract mid-renegotiation — where the honest answer is to duplicate with a comment naming the date, rather than to pick a side.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Testing & Reliability Engineering — when one rule must be stated in two runtimes, a contract test between them is the only mechanism that turns silent drift into a failing build.