Skip to content

9.3.2 — DRY: Don't Repeat Yourself

The delivery app charges 5% commission to restaurants. That number lives in four places, and nobody planned it that way.

typescript
// payouts.ts
const commission = order.total.cents * 0.05;                    // (1)

// invoice-pdf.ts
const line = `Commission (5%): ${fmt(order.total.cents * 0.05)}`;  // (2)

// analytics/revenue.ts
SELECT SUM(total_cents * 0.05) AS commission FROM orders          // (3)

// restaurant-dashboard.tsx
<p>You keep 95% of every order.</p>                               // (4)

Line (1) is the number that actually moves money. Line (2) prints it on the invoice. Line (3) is a database query the finance team runs. Line (4) is a sentence on a web page.

Now the business signs a deal: restaurants in Pune pay 3% for their first six months. An engineer changes line (1), tests the payout, and ships. The invoice still says 5%. The finance dashboard still reports 5%. The web page still promises 95%.

Nothing crashed. No test failed, because the tests were written around line (1) too. The bug is discovered six weeks later by a restaurant owner comparing their bank statement to their invoice, and by then there are four hundred wrong invoices and a support queue.

That is what DRY is about, and notice what the real problem was. It was not that 0.05 was typed four times. It was that one business decision had four homes, and only one of them was updated.

1. What the principle actually says

The usual phrasing is "don't repeat yourself", which people hear as "never type the same thing twice". That reading causes as much damage as it prevents. The original wording, from Andy Hunt and Dave Thomas, is more careful and much more useful:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

Read the first two words again. Piece of knowledge. Not piece of code. Not piece of text.

The commission rate is a piece of knowledge. It belongs to the business, it changes when the business decides, and there should be exactly one place in the entire system that states what it is. When it has four homes, a change to the decision requires four edits, and any one of them can be missed silently.

So the fix is not "extract a function because two lines look alike". The fix is: find the knowledge, give it one home, and make everything else ask.

typescript
// pricing/commission.ts — the one place that knows
export const COMMISSION = {
  standard: Percentage.of(5),                                    // (1)
  introductoryPune: Percentage.of(3),
};

export function commissionFor(order: Order): Money {             // (2)
  const rate = isIntroductoryPune(order.restaurant)
    ? COMMISSION.introductoryPune
    : COMMISSION.standard;
  return rate.of(order.total);                                   // (3)
}

Line (1) states the rates once, as Percentage value objects rather than bare numbers, so a reader can never mistake 5 for "five rupees" (9.2.1 covers why value objects beat raw numbers).

Line (2) is the single answer to "what commission does this order pay". Every screen, every invoice and every payout now calls this.

Line (3) returns Money, so the caller cannot accidentally treat a rate as an amount.

The database query in line (3) of the original is the awkward one, because SQL cannot call your TypeScript. That is a real problem and it has a real answer: the report should read a stored commission_cents column that was written by this function when the order was created, rather than recomputing the rule in a second language. When two languages both need a rule, store the result, not the rule.

2. The trap: code that looks alike but is not the same knowledge

Now the failure that comes from applying DRY too eagerly, which is more common among careful engineers than the original problem.

typescript
// Two functions. They look identical. Merge them?
function validateSignupPassword(pw: string): boolean {
  return pw.length >= 8;
}

function validateWifiPassword(pw: string): boolean {
  return pw.length >= 8;
}

Six characters of difference, same body. DRY, read as "never write the same code twice", says merge them into validatePassword. So somebody does.

Eight months later, security requires signup passwords to be twelve characters. The shared function changes, and now the office wifi password must also be twelve characters, which was never the intent and which nobody notices until the router rejects it.

The engineer who fixes that will not un-merge the function. They will add a parameter:

typescript
function validatePassword(pw: string, minLength: number): boolean { … }

Then a flag for "must contain a digit, but only for signup". Then another. Within a year, the shared function is a small maze of options, and understanding either caller means understanding both.

The two functions were never the same knowledge. One encodes a security policy for customer accounts. The other encodes a router's configuration limit. They were the same number, by coincidence, on one particular day. Coincidence is not a reason to bind two things together forever.

The test that separates real duplication from coincidence:

Will these two pieces of code always need to change at the same time, for the same reason?

Yes means they are one piece of knowledge and belong in one place. No, or "not sure", means leave them alone. The commission rate passes this test: if the rate changes, every one of those four places is wrong immediately. The password lengths fail it: the two can move independently and eventually will.

3. Three sibling ideas worth knowing by name

WET is the joke acronym for the opposite, expanded either as "write everything twice" or "we enjoy typing". It is used as a criticism, but there is a serious version: the first duplication is often correct, because you cannot yet see which parts are the shared idea and which are accidental.

The rule of three says: write it once. When the same thing appears a second time, notice and wait. On the third occurrence, extract. The reasoning is straight geometry — two points can look like a line by accident, three rarely do. With three real examples in front of you, the shape they genuinely share is visible, and the abstraction you build will fit all three instead of being a copy of the first one with holes cut in it.

AHA stands for "avoid hasty abstractions", from Kent C. Dodds, and it is the summary of this whole page: prefer duplication over the wrong abstraction, and wait until the abstraction is obvious. It is the same claim Sandi Metz makes in 9.1 section 6, and the reason it is worth repeating is the asymmetry underneath. Removing duplication later is easy, local, and mechanical. Removing a wrong abstraction later is hard, because everything that uses it has already shaped itself around it.

4. The kinds of duplication that actually hurt

Not all repeated things are equal. Ranked by how much damage they do:

Duplicated business rules — always fix. The commission rate. The rule for when an order can be cancelled. The definition of "active customer". These change for exactly one reason, and a missed copy is a wrong answer given confidently to a customer.

Duplicated knowledge across boundaries — fix by generating. The same shape described in your TypeScript types, your database schema, and your API documentation. Three descriptions of one truth, drifting apart quietly. The cure is not discipline, it is generation: derive two of them from the third so drift is impossible (9.6.4 covers this for APIs).

Duplicated structure with different meaning — usually leave. Two forms that both have three fields. Two functions that both loop and sum. They resemble each other and share no reason to change.

Duplicated boilerplate the language forces on you — leave it. Import lines, constructor parameter assignments, the shape of an error class. Hiding these behind machinery costs more in confusion than it saves in keystrokes.

The pattern across those four: duplication of meaning is a defect. Duplication of shape is usually fine.

5. When DRY is the wrong move

Across services or teams that must be able to move independently. This one surprises people. If two services share a library of business logic, they now deploy together, argue about upgrades, and cannot change their own rules without a negotiation. Copying a hundred lines is frequently the cheaper answer, and it is a deliberate, defensible choice rather than sloppiness (Part 10.8 covers it in the microservices setting).

In tests, up to a point. A test that reaches through four helper functions to set up its data is a test nobody can read, and an unreadable test is worse than a repetitive one, because when it fails at 2 a.m. somebody has to understand it fast. Some repetition inside tests is a feature.

When the shared thing would need to know who is calling it. The moment a shared function needs an if (caller === "signup"), the merge was wrong. That if is the two ideas trying to separate again, and it will keep growing.

When the duplication is small and the coupling would be large. Three lines copied twice is cheap. A shared base class that both callers must now satisfy forever is not.

6. How this comes up in interviews

The question is usually "what does DRY mean?" and the weak answer is "don't repeat code". The strong answer has three parts and takes about forty seconds:

"DRY is about knowledge, not text. Every piece of knowledge should have one authoritative home, so a commission rate or a cancellation rule lives in exactly one place and everything else asks. What it does not mean is merging code that merely looks similar. Two functions can be identical today and change for completely different reasons tomorrow, and once you have merged them, the divergence arrives as boolean parameters and the shared function becomes harder to change than the duplication ever was. So the test I use is whether the two will always change together, for the same reason. If yes, merge. If not, wait — the rule of three is a decent default, and duplication is cheaper to fix later than a wrong abstraction."

The follow-up is usually "when would you deliberately duplicate?" Answer with the service boundary case, because it shows you have thought about it beyond the class level.

Recall

  • DRY's real wording is "every piece of knowledge must have a single, unambiguous, authoritative representation" — knowledge, not text. The commission rate in four files is one decision with four homes, and only one gets updated.
  • The test that separates real duplication from coincidence: will these always need to change at the same time, for the same reason? Yes means merge. No or unsure means leave them.
  • Two identical password checks are not one rule — one is a security policy, the other a router limit. Merging them means the divergence arrives later as boolean parameters, and the shared function becomes a maze.
  • Rule of three: extract on the third occurrence, because two points can look like a line by accident. AHA — avoid hasty abstractions — because removing duplication later is easy while removing a wrong abstraction is not.
  • Ranked by damage: duplicated business rules always get fixed; duplicated knowledge across boundaries (types, schema, docs) gets fixed by generating one from another; duplicated structure with different meaning is usually fine; forced boilerplate is left alone.
  • When two languages need the same rule, store the result, not the rule — the report reads a stored commission column rather than recomputing the formula in SQL.
  • Deliberately duplicate across services that must deploy independently, inside tests where readability wins, and whenever the shared thing would need to know who called it.

Self-test: State DRY using the word "knowledge" and explain why that word matters. Give the one-sentence test for real versus coincidental duplication. Why is a wrong abstraction more expensive than duplication? Name two situations where you would copy code on purpose.

Quiz Bank

InterviewWhat does DRY actually mean, and when should you deliberately not apply it?

What it means. The real formulation is that every piece of knowledge must have a single, unambiguous, authoritative home. The word doing the work is knowledge. A commission rate, a cancellation rule, the definition of an active customer — each is one business decision, and if it is written down in four places then changing the decision needs four edits and any missed one produces a confidently wrong answer for a customer.

What it does not mean. It is not "never type the same characters twice". Two functions can be byte-identical today and exist for entirely different reasons. Merge them and the first divergence arrives as a boolean parameter, then a second, until understanding either caller requires understanding both plus every flag. The test to apply is whether the two will always need to change at the same time, for the same reason. If not, they are not duplication, they are a coincidence.

When to deliberately not apply it. Across services that must deploy independently, because a shared library means shared release timing and a negotiation before either team can change its own rules. Inside tests, where a little repetition keeps a failing test readable at 2 a.m. and a chain of setup helpers does not. When the shared code would need to know who called it — the first if (caller === …) proves the merge was wrong. And when the duplication is three lines while the coupling would be a base class both sides must satisfy forever.

The default to state: rule of three. Notice on the second occurrence, extract on the third, because by then the shape they truly share is visible instead of guessed.

AppliedA business rule lives in TypeScript and is also recomputed in a SQL report. How do you make that DRY?

You cannot share the function, because SQL cannot call TypeScript, so the usual answer does not apply and people tend to shrug and accept the drift. There is a better move.

Store the result, not the rule. When the order is created, the application computes the commission with the one authoritative function and writes the answer into a commission_cents column. The report then reads that column instead of recomputing anything. The rule still has exactly one home; the database holds an outcome rather than a duplicate of the logic.

This has a second benefit that matters more than the first. Rates change over time. If the report recomputes today's rate over last year's orders, every historical number silently changes the day the rate changes, which is wrong — those orders really were charged the old rate. Storing the computed amount makes history immutable, which is what finance and auditors actually need. So the DRY fix and the correctness fix are the same fix.

When you genuinely cannot store it — an ad-hoc analytical query over data that predates the column — the fallback is to generate the SQL fragment from the same source as the code, or at minimum to keep a single reconciliation check that compares the two paths on a sample and alerts when they disagree. That is not as good, and it should be labelled as a known risk rather than treated as solved.