Don't repeat yourself (DRY)
Open in the interactive app: practice, quiz, mock interview →In one minute
- DRY ("Don't Repeat Yourself") says every piece of knowledge in a system should have one authoritative representation. It is about knowledge (a rule, a format, a limit), not about lines of text that happen to look alike.
- Duplicated knowledge drifts: someone changes the tax rule in the backend and not in the checkout page, and the two disagree in production.
- Duplicated text that encodes different knowledge is fine. Merging two lookalike functions that change for different reasons creates the wrong abstraction, which is more expensive than the copy.
- The strongest DRY tool is often generation: one schema produces the validator, the docs, the client types and the database migration.
- Agents duplicate freely, because pasting a working snippet is the shortest path to green tests. They also over-abstract when asked to "clean up". Review both directions: is this the same rule written twice, or two rules that only look alike today?
How it works
The definition is about knowledge
The phrase comes from The Pragmatic Programmer (Hunt and Thomas, 1999), which defines it as "every piece of knowledge must have a single, unambiguous, authoritative representation within a system". The key words are knowledge and authoritative. A shipping-weight limit of 30 kg is knowledge. If it appears as 30 in the API validator, as 30000 grams in the warehouse service and as "up to 30 kg" in the help page, the system holds three copies of one fact, and nothing forces them to agree.
Code, schemas, configuration, documentation, tests and database constraints can all hold knowledge. DRY applies to all of them, which is why a principle that sounds like "extract a function" is really about where facts live.
What drift looks like
# checkout/api.py
def can_ship(weight_g):
return weight_g <= 30_000
# warehouse/labels.py
MAX_PARCEL_KG = 30
def label_class(weight_kg):
return "heavy" if weight_kg > MAX_PARCEL_KG * 0.8 else "standard"Then the carrier raises the limit to 32 kg. The team updates MAX_PARCEL_KG, and checkout keeps rejecting parcels between 30 and 32 kg for a month. Each copy was correct when written. The defect is that they were copies.
The fix is one owner of the fact, shipping_limits.MAX_PARCEL_GRAMS = 32_000, imported by both. When the services are separate deploy units, the owner might be a config service, a shared schema, or one service exposing the limit through its API.
Coincidental duplication
Two functions can be textually identical and still represent different knowledge:
def free_shipping_eligible(order_cents):
return order_cents >= 5_000
def loyalty_bonus_eligible(order_cents):
return order_cents >= 5_000Marketing owns one threshold, the loyalty team the other. Merging them into over_fifty_dollars() couples two policies that will diverge next quarter. The test is the reason to change: if the two would always change together, they are one piece of knowledge; if they change for different reasons and by different people, they are two, however alike they look.
The wrong abstraction
Sandi Metz describes the usual decay: someone extracts a shared helper; a new caller needs something slightly different, so a parameter is added; then a flag; then a branch for a third caller. After a year the helper is a switch statement over callers, each change risks the others, and nobody dares touch it. Her advice is to inline the abstraction back into its callers and let them diverge, then look again for what is truly shared. The rule of three is the practical version: tolerate the second copy, extract on the third, when you can see what actually varies.
Generate instead of copy
Some knowledge must exist in several forms: a request format appears as server validation, client types, API docs and a database column. Writing each by hand is four copies. Generating them from one definition (Protocol Buffers, OpenAPI, JSON Schema, an ORM model) keeps one authoritative source and lets tools produce the rest. The practice task builds a small version: one field schema that drives both validation and documentation.
Reviewing agent-written code
A coding agent asked to add a "premium" plan often copies the "standard" plan's pricing function and edits two numbers. That is duplication of the rounding and proration rules, which will drift. Asked to "reduce duplication", the same agent may merge two unrelated validators behind a mode parameter. Ask, for each copy or each merge: what is the fact here, who owns it, and will these change together?
The same shape elsewhere
DRY is the multi-step consistency problem in source code. Two copies of a fact behave like two replicas of a database row without replication: they agree at write time and diverge silently afterwards. The remedies are the same as in data systems. Keep one leader (a single source of truth), derive the rest from it (generation, like a materialised view), or detect divergence (a test that compares the copies, like an anti-entropy check).
Down the stack
Layer 1The contract
A DRY design promises that changing a piece of knowledge requires editing one place, and that every consumer sees the change after the next build or deploy. Testable form: grep for the fact (the literal, the regex, the field name) and find one definition; change it and watch every dependent test respond. The flip side of the contract is coupling: every consumer of the shared definition now changes when it changes, whether it wanted to or not.
Layer 2The mechanism
Languages offer several ways to keep one copy, each with a different cost. A function is the simplest. Generics remove duplication across types, and are implemented very differently: C++ templates and Rust generics are monomorphised, meaning the compiler emits a separate machine-code copy per type; Java erases type parameters to one shared body that works on Object references; Go uses "GC shape stenciling", producing one body per memory shape (all pointer types share one) plus a dictionary argument that carries type information. Code generators (protoc, OpenAPI generators, sqlc) produce source from a schema at build time. Shared libraries move a definition into a separate package that several programs import.
Each of these adds an edge to the build graph. In a monorepo built with Bazel or a similar tool, a change to a widely shared package invalidates every target that depends on it, so editing one constant in a common library can rebuild and retest thousands of targets. Across services, a shared library ties deploy units together: to change the shared model, every service that embeds it must be rebuilt and redeployed, or run with mismatched versions in the meantime.
Layer 3The machine
Extracting a helper adds a call: a few nanoseconds at most, and usually zero, because compilers inline small functions. Go inlines functions under a cost budget of roughly 80 nodes of its intermediate representation; HotSpot inlines hot methods up to a bytecode size threshold of a few hundred bytes. So the compiler often undoes your DRY at the machine level by copying the helper into each caller, which is fine: duplication in machine code is cheap and does not drift.
Monomorphisation is the same trade made deliberately. Each instantiation of a C++ template gets its own specialised, fast code, at the price of binary size and instruction-cache pressure; a heavily templated library can add megabytes to a binary. Go's shape-based approach keeps binaries smaller but routes some method calls through the dictionary, which can be slower than a direct call in hot loops. Dynamic linking is DRY at the operating-system level: one copy of libc lives in the page cache and is mapped read-only into every process, so 200 processes share one set of physical pages instead of 200.
Why it's built this way
- Facts change, and every copy of a fact must change with it, so copies multiply the cost of change and the risk of missing one.
- So keep one authoritative copy and derive the others.
- But every derivation is a dependency, and dependencies couple the things that share them.
- So apply DRY to knowledge that genuinely changes together, and accept duplicated text, or let the compiler duplicate machine code, where coupling would cost more than drift.
Diagram
flowchart LR S["order.schema (one source)"] --> G["code generator"] G --> V["server validator"] G --> T["client types"] G --> D["API docs"] G --> M["DB migration"] V --> API["checkout service"] T --> WEB["web app"] CHK["CI: regenerate and diff"] --> S
Worked example
An online store computes discounts in two places: the TypeScript cart shows a price as the shopper builds an order, and the Go payment service charges the card. Both implement "10 percent off orders over 100 dollars, rounded to the cent". The cart rounds each line item and sums; the payment service sums and then rounds. For three items at 33.35 dollars, the cart discounts each line to 30.015, rounds it to 30.02 and shows 90.06; the payment service discounts the 100.05 total to 90.045, rounds once and charges 90.05. Across 40,000 orders a day, about 0.6 percent end with the displayed and charged totals a cent apart: around 240 orders and a steady trickle of support tickets asking why the price changed at payment.
The team considered sharing a library between the two codebases, which is awkward across languages and would couple the web release to the payment release. They chose a single source of truth instead: the payment service exposes a quote endpoint that returns the exact total, and the cart calls it on each change (about 20 ms per call). The discount rule now exists once. For the offline preview, the cart shows "about 90 dollars" until the quote returns. Mismatch tickets went to zero, and the next pricing change touched one file.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Duplicate the code | No coupling, each copy evolves freely | Copies drift when the knowledge is shared | The copies encode different knowledge, or you have only two |
| Extract a function or module | One place to change; cheap after inlining | Couples every caller to one definition | Same knowledge, same reason to change, same deploy unit |
| Generate from a schema | One source across languages and layers | Build step, generator quirks, generated code to read | Formats shared across services, clients and storage |
| Shared library across services | Real code reuse | Couples deploy units; version skew | Stable, slow-changing utilities (logging, auth clients) |
| One service owns the fact | Single runtime source of truth | Network hop, availability dependency | Business rules such as prices, limits and entitlements |
Pitfalls in production
- The same validation regex in frontend, backend and database, with one copy stricter than the others. Fix: generate the client check from the server's schema, or make the server the only authority and treat the client check as a hint.
- A
commonlibrary that every service imports and every team edits, so one change forces a fleet-wide redeploy. Fix: split by reason to change; copy small utilities instead of sharing them. - Helpers with boolean flags for each caller (
format(x, is_invoice, legacy_mode)). Fix: inline back into callers, then extract only what is genuinely common. - Constants duplicated in tests, so a changed limit leaves the test asserting the old value and passing against stale code. Fix: tests import the constant, or assert on behaviour at the boundary.
- Documentation that restates code and goes stale. Fix: generate reference docs from the source, and keep hand-written docs to the reasons behind the code.
Leadership lens
- Ask "who owns this fact?" for every business rule: prices, limits, retention periods, feature eligibility. If the answer is "it is in three places", plan to consolidate it.
- Weigh a shared library by its change rate and number of consumers. A fast-changing library shared across ten deploy units turns every edit into a coordinated release.
- Budget for code generation as infrastructure: someone must own the generator, the CI check that generated files are up to date, and upgrades.
- When AI agents produce code, duplication is the cheapest output. Add a review question ("is any of this knowledge already defined somewhere?") and a lint or search step for repeated literals in domain code.
- Do not reward "lines removed" from deduplication on its own. A merge that couples two teams' policies is a regression even if the diff is negative.
Check yourself
Two functions have identical bodies. Under what condition should you leave them duplicated?
Show answer
When they encode different knowledge that changes for different reasons or is owned by different people. Merging them would couple policies that will diverge.
Why can monomorphised generics increase binary size, and why is that not a DRY violation?
Show answer
The compiler emits a separate copy of the code for each type instantiation. The source still has one definition, so there is one place to change; the machine-code copies are derived and cannot drift.
A constant shared through a library used by 12 services needs to change. What does DRY cost you here?
Show answer
Every service must rebuild and redeploy to pick up the change, and until then services disagree. If the value changes often, it belongs in configuration or behind one service's API instead.
What is the rule of three, and what problem does it guard against?
Show answer
Allow a second copy and extract a shared abstraction only at the third. It guards against premature abstraction built from too few examples to see what actually varies.
Watch
- All the Little Things (Sandi Metz, RailsConf 2014): refactoring tangled conditionals step by step, and why duplication is cheaper than the wrong abstraction.
Free resources
- Orthogonality and the DRY Principle (Artima interview with Dave Thomas): the authors of the principle explain that it is about knowledge, not code.
- The Wrong Abstraction (Sandi Metz): how shared helpers decay into flag-driven tangles, and how to back out.
- Rule of three (Wikipedia)): the heuristic for when to extract duplicated code.
- AHA Programming (Kent C. Dodds): "avoid hasty abstractions" as a counterweight to aggressive deduplication.
- Go Proverbs: includes "a little copying is better than a little dependency", the cross-package side of the trade-off.
- Don't repeat yourself (Wikipedia): background, the opposing WET label and related principles.