DRY Principle
Apply DRY thinking to every programming task — not as a pedantic rule, but as a design instinct.
DRY means every piece of knowledge has a single, authoritative representation in the system.
The key word is knowledge, not code. Two identical-looking code blocks might represent different
knowledge (and should stay separate). Two different-looking blocks might encode the same business
rule (and should be unified).
The test: "If this knowledge changes, how many places do I need to update?" If the answer is
more than one, that's a DRY violation worth examining.
Run this as a multi-pass sweep
The different kinds of duplication don't just look different — they're found by
different search motions, and that's the catch. Magic values surface when you scan literals.
Knowledge duplication surfaces when you ask what changes together. Wiring duplication surfaces
when you compare interaction shapes across siblings. Symbol↔label duplication surfaces when
you match a typed token against a bare string in another file. Same-file scattered duplication
surfaces when you read look-alike function bodies side by side. Per-instance duplication
surfaces only when you picture the component multiplied across every screen, row, or tab it
renders into — it's invisible in the source, which appears exactly once. These are genuinely
different mental questions, and the moment you lock onto one — say, hunting magic numbers — you stop
seeing the others, because you're no longer asking their question. This is why a single
read-through reliably catches the loudest one or two categories and silently walks past the
rest. It isn't that the rest are subtle; it's that you weren't looking with the right lens.
So don't try to see everything in one look. Make one focused pass per lens, switching the
question you ask each time, and keep a running note of which passes you've completed so you
don't circle the same ground. A later pass routinely finds things an earlier one couldn't —
not because they were hidden, but because you were asking a different question. The passes
(each links to its worked detail in references/patterns.md):
- Knowledge & business rules — the same policy, validation, or calculation in more than
one place; parallel structures kept in sync by hand; redundant or derivable state; scattered
config. Ask: "if this rule changes, how many places do I edit?" One variant the other
(similarity-based) lenses structurally miss: co-indexed structures — 3+ collections keyed
by the same identity, mutated across several sites. They drift into incomplete copies (one
teardown path clears three of four maps, another skips a set, a third is missing the deletes
entirely), so the sites stop looking alike and read as "different code" — and the defect at a
site is an absence, which no repetition scan can see. Detect it top-down: enumerate the
co-indexed collections, then flip the question from "are these blocks similar?" to "does every
site that touches one of the group touch all of them?" A subset site is the smell — and
usually a latent desync bug, not just waste. → patterns.md › Knowledge duplication.
- Per-instance / fan-out state — screen-independent knowledge (a fetch, a timer, a cache, a
"pinned" / "selected" flag) living inside a component the framework instantiates per screen /
row / tab. The source appears exactly once, so this is invisible to every text-based lens —
it only duplicates at runtime, and it's the one that's also a latent desync bug, not just
waste. Ask: "if there were two monitors or ten rows, would this run or store twice — and does
it need to?" → patterns.md › Per-instance and fan-out duplication.
- Magic values & boundary literals — unnamed literals that carry meaning;
0 / 1 / -1
comparisons that really ask a semantic question; stringly-typed code ignoring an enum that
already exists. Ask: "does this literal mean something, and does a name for it already
exist?" → patterns.md › Code-level duplication.
- Repeated logic, parameters & orchestration — copied blocks with one or two values
changed; parameter sprawl; the same
begin/try/commit/rollback or load→auth→authorize
wrapped around a varying call. Ask: "what did every caller do around the interesting
line?" → patterns.md › Code-level duplication (repeated logic, parameter sprawl) and
Call-site duplication.
- Cross-file siblings, wiring & symbol↔label — 3+ sibling files exposing the same outward
shape (signal triples, event sets, prop interfaces, repeated UI elements/layouts/tokens); a
handler's token versus its user-facing label/route/flag living as a bare string in another
file. Ask: "do the siblings share an interface? does this name reappear as a literal
somewhere else?" → patterns.md › UI components, Interaction and wiring, Symbol / label
duplication.
- Same-file scattered & beyond-code — the same 4-line block embedded in three functions of
one file; duplicated test setup, config, docs, or schema constraints. Ask: "read the
look-alike bodies side by side — what repeats?" → patterns.md › Beyond code (same-file
scattered scanning is in "How to apply", below).
Scale the sweep to the work: a one-line fix collapses to near-nothing (a quick glance and
you're done), but a real review or refactor earns all six deliberate passes. When you
finish, fold the hits from every pass into one consolidated set of findings rather than
reporting each pass separately — see "Communication".
When the codebase is large enough that you fan the sweep out across several readers or
subagents, partition the work by concern — data flow, wiring, theming, per-instance state —
not by directory. A module-by-module split feels tidy but is blind to exactly the cross-module
and runtime-instantiation duplication that hides best; each reader sees one folder and every
file in it reads fine on its own. Carry a short ledger of what you rejected and why from one
pass to the next, so a later pass doesn't re-flag a coincidental match you already cleared. You
know you've converged when a fresh deep pass turns up only one-line nits — literal zero is never
provable on a live codebase, so stop when the angles run dry, not when you've "proven" emptiness.
For a whole-repository audit specifically — "find all the duplication in this codebase", a large
refactor, a pre-release cleanup — fan the six passes out one agent per lens (giving each
agent a single question is what stops it drifting across lenses). Two ways to run it:
- Deterministic (preferred): call the
Workflow tool with the bundled script. A request for
a whole-repo DRY audit is itself the opt-in — this skill instruction authorizes the call, so
run it directly: don't ask the user to confirm again, and don't downgrade to the fallback to
"skip the round-trip." Workflow({ scriptPath: "<this skill dir>/scripts/dry-sweep.js", args: { scope: "<repo or path>", patternsPath: "<this skill dir>/references/patterns.md" } }) spawns
exactly six fixed-label lens agents in parallel, then one merge agent (cross-lens dedup,
Rule-of-Three count, leverage-ranked report) — identical structure every run.
- Fallback (guidance only): use this only when the
Workflow tool is genuinely unavailable
in this environment. Then do it by hand following references/parallel-sweep.md — same agent
tree, but the model improvises the spawn, so the shape varies run to run.
Either way, read references/parallel-sweep.md for the why: the rule that cross-cutting lenses
must keep whole-repo view rather than being directory-split, and why the Rule-of-Three count has
to live in the merge step. Don't reach for any of this on an ordinary edit — the fan-out only
pays off at repo scale.
Once you have a batch of findings to act on, read references/applying-fixes.md before
touching code. Finding duplication is the safe half; removing it is where DRY regresses — a
hoist that changes lifecycle, a "shared" helper that merged two things which only looked alike,
a singleton hoist that globalizes state which should have stayed per-instance. That reference
covers the execution order (foundational and subsuming fixes first, so you don't polish code
you're about to delete), the failure mode specific to each fix type and how to check it, and the
step-by-step singleton hoist for the per-instance lens — the one fix that changes runtime
structure rather than text, so it's the easiest to get wrong.
What to look for
The six lenses above, under "Run this as a multi-pass sweep", are the scan index — each names
what to look for and the question that surfaces it. The full catalogue behind them — every
duplication family with before/after examples across languages and frameworks (knowledge,
per-instance, code-level, beyond-code, UI, wiring, symbol↔label, call-site) — lives in
references/patterns.md. Read it when doing a real review or audit; an ordinary edit only needs
the lens list and tests above. The guardrails below apply on every task regardless.
Guardrails: when NOT to deduplicate
DRY's biggest failure mode is premature or wrong abstraction. These guardrails are just as
important as the principle itself.
A candidate is a hypothesis, not a finding
A duplication you spotted by pattern — two blocks that look alike, the same literal in three
files — is a hypothesis, not a finding. Before you report it or act on it, open every site and
read it in full, including the surrounding code, and actively try to disprove the match.
Surface similarity is cheap to notice; the value of the review is the deeper look that reveals
whether the dedup is actually possible. More often than you'd expect, a factor visible only in
the real code disqualifies the merge:
- One site has an error path, a retry, an exit-code check, or an edge case the others lack.
- Two blocks read alike but consume different outputs — one needs a parser on stdout, the other
only the exit status — so unifying them forces a worse interface onto both.
- The literals are near but not equal (
-0.3 vs -0.03): typo-level proximity, not a shared
value. Merging couples two unrelated constants.
- The divergence is intentional — noted in a comment, an established convention, or project
memory. "Fixing" it reintroduces the bug the divergence was working around.
- The two change for different reasons (the coincidental-similarity test below); they only look
alike at this moment in time.
Spend the effort to find that factor before you flag anything. A candidate that survived a
genuine attempt to disprove it is worth reporting; a surface match passed along unchecked is
noise — it wastes the reader, and if acted on, it breaks working code. When the deeper look
does disqualify a candidate, record why: that reasoning is as valuable as the findings, and
it stops the next pass (or the next reviewer) from re-flagging the same thing.
The Rule of Three
Two instances of similar code are not enough evidence to abstract. Wait for three occurrences —
by the third time, the real pattern is visible and you can design a good abstraction. On the
first or second occurrence, note the similarity but leave it alone unless the duplication is
clearly the same piece of knowledge.
Coincidental similarity is not duplication
Two code blocks can look identical today but exist for different reasons. Ask: "Do these change
for the same reason, at the same time, by the same person?" If not, they represent different
knowledge that happens to look alike right now. Merging them creates coupling between unrelated
concerns — when one changes, the shared abstraction must accommodate both, harming both.
Example: A validateUserInput() and validateAPIResponse() might have identical checks
today. But user input validation changes when the UI changes, while API response validation
changes when the upstream API changes. These are different pieces of knowledge — keep them
separate.
The wrong abstraction is worse than duplication
Sandi Metz: "Prefer duplication over the wrong abstraction." Watch for these warning signs
that an abstraction has gone wrong:
- A shared function accumulating boolean parameters and conditional branches to handle
"slightly different" callers
- A base class where subclasses override most methods
- A "utility" module that every file imports but each uses differently
- Adding a new feature requires modifying shared code in a way that might break other callers
When you spot these, the fastest way forward is often back: inline the abstraction into its
callers, remove what each caller doesn't need, then re-examine whether a genuine shared
pattern exists.
YAGNI complements DRY
Don't build abstractions for hypothetical future duplication. "We might need this in three
other places someday" is not a reason to abstract today. Abstract when you actually have the
duplication, not when you imagine you might.
How to apply this
When writing new code
Before writing, search for existing helpers. Scan the places this codebase keeps
shared knowledge:
- The file you're editing and its adjacent siblings in the same directory
- Utility / helper / shared modules (common names:
utils/, lib/, common/, shared/,
helpers/)
- Type and constant modules (
constants.ts, types.ts, enums.rs) — existing string
unions, enums, or branded types the literal you're about to hardcode may already live in
- Central homes for cross-cutting concerns (auth, logging, HTTP clients, db helpers,
feature flags)
- The standard library. Before writing a helper that does generic data manipulation —
cloning a map, checking membership, equality, sorting, reversing, finding, mapping,
filtering, deduping, batching — check whether the language's stdlib already provides it.
This is especially worth a check for newer stdlib additions: Go's
maps.Clone /
slices.Contains / cmp.Or, Python's itertools.batched / functools.cache,
JavaScript's Array.prototype.findLast / Object.groupBy, Rust's slice::is_sorted —
these regularly replace hand-rolled utilities that were written before the stdlib caught
up. A 4-line helper that wraps a single stdlib call is duplicated knowledge with
extra steps.
If a helper, constant, config value, or component already does what you need, use it. If
one almost does, consider extending it rather than writing a parallel version — but only
when the two callers really share the same knowledge (see "Coincidental similarity" above).
If you're about to write something that looks like existing code, pause and ask: is this
the same knowledge? If yes, extend or reuse the existing implementation. If no (different
reasons to change), write it fresh.
Extract shared components before duplicating them. If you know two callers need the
same thing, write the shared version first.
Scan for magic values. Before finishing, review your code for any literal numbers or
strings that carry meaning — timeouts, thresholds, status codes, sizes, URLs, format
strings. Don't skip small values: 0, 1, -1 in comparisons often encode state
boundaries that deserve a name or helper. Name them. If the same value already exists
as a constant elsewhere, reuse it.
Scan for repeated patterns. If you just wrote a block of logic that mirrors something
nearby, extract a helper function immediately rather than leaving two copies.
When reviewing or modifying existing code
This is where the multi-pass sweep earns its keep — run the passes from "Run this as a
multi-pass sweep" over the area you're touching, switching the question each pass. The points
below are the same lenses stated as actions:
Notice duplication in the area you're touching. You don't need to fix every DRY violation
in the codebase — focus on the code you're already changing.
If you find duplicated knowledge that affects your change, flag it. Suggest a concrete
refactor if the path is clear, or note it as tech debt if the fix is complex.
If you're about to copy-paste-modify, stop. Can you parameterize the existing code instead?
But only if the variation represents the same underlying knowledge.
Look sideways before finishing. When the module you're touching has 3+ sibling files
at the same level (components in components/, handlers in a route package, services
under services/, structs implementing the same trait, views in an MVC app), take 30
seconds to diff their interface surfaces — exported functions, emitted events, signal
names, method signatures, prop types. If three siblings expose the same outward shape in
parallel (same signal triples, same event set, same prop interface), that's the Rule of
Three across files, and it's exactly the archetype the rule was designed to catch.
In-file review misses this every time because each file reads fine on its own. This is the
cross-file pass (pass 5) — if your earlier passes keep coming up empty but something still
feels off, that's the signal to widen the lens: the duplication lives between files rather
than within one, and only a sideways diff of sibling interfaces will surface it.
Cross-check handler ↔ label pairs. For every named action the module wires — a key
binding, route, CLI flag, command, event, metric, env var, permission, localization key —
confirm the user-visible name comes from the same source as the handler matches on, not
a parallel string literal in a sibling file. This duplication is invisible to
structural diffs because the two sides aren't shaped alike (typed match arm vs. bare
string), so it survives narrow DRY sweeps. See patterns.md › Symbol / label duplication.
Scan within a single file for scattered duplication. Cross-file scanning catches
"same function name in two packages." Within-file scanning catches "same 4-line block
embedded inside three different functions of the same file." The latter is invisible to
anyone reading one function at a time and easy to miss when reviewing diffs that touch
only one function. The trick: when you're in a file with several similarly-shaped
functions (multiple summarize*, format*, parse*, handle*, or all the methods on a
struct), pull up each one and read the bodies side-by-side. Identical literal boundaries
(if len(runes) > 40), identical setup/teardown shapes, identical error-formatting calls
embedded in different functions are the typical hit. Same-file scattered duplication is
especially common in:
- Utility modules with many small functions
- Formatter/renderer/serializer modules where each function handles a different type
- Handler/dispatcher modules where each case has its own preamble
- Test files (DRY the setup, not the intent — see patterns.md › Beyond code)
When fixing bugs
A bug that exists in duplicated code probably exists in every copy. After fixing it in one
place, search for other instances of the same logic. If you find copies, consider whether
this is the right time to unify them — but only if they genuinely represent the same knowledge.
Communication
When you spot DRY issues, be specific and practical:
- Name what knowledge is duplicated and where
- Explain whether it's worth fixing now or just noting
- If recommending a refactor, show the concrete approach
- If recommending against deduplication (coincidental similarity, wrong abstraction risk),
explain why the duplication is actually healthy
Don't lecture about DRY in the abstract. Apply it through your actions — write DRY code,
flag violations when relevant, and protect against over-abstraction. The goal is better
software, not adherence to a principle for its own sake.
1---2name: dry-principle3description: Enforces DRY (Don't Repeat Yourself) thinking across all programming tasks — writing new code, reviewing code, fixing bugs, refactoring, editing config, schemas, tests, and documentation. Use this skill for ANY coding or programming-related task: feature implementation, bug fixes, code review, refactoring, writing tests, editing configuration, database schema changes, build system modifications, or documentation updates. This skill should trigger whenever Claude is about to write, modify, or review code or code-adjacent files. Even if the user doesn't mention "DRY" or "duplication", this skill applies to all software engineering work.4---56# DRY Principle78Apply DRY thinking to every programming task — not as a pedantic rule, but as a design instinct.9DRY means every piece of **knowledge** has a single, authoritative representation in the system.10The key word is knowledge, not code. Two identical-looking code blocks might represent different11knowledge (and should stay separate). Two different-looking blocks might encode the same business12rule (and should be unified).1314The test: "If this knowledge changes, how many places do I need to update?" If the answer is15more than one, that's a DRY violation worth examining.1617## Run this as a multi-pass sweep1819The different kinds of duplication don't just *look* different — they're found by20different search motions, and that's the catch. Magic values surface when you *scan literals*.21Knowledge duplication surfaces when you *ask what changes together*. Wiring duplication surfaces22when you *compare interaction shapes* across siblings. Symbol↔label duplication surfaces when23you *match a typed token against a bare string in another file*. Same-file scattered duplication24surfaces when you *read look-alike function bodies side by side*. Per-instance duplication25surfaces only when you *picture the component multiplied across every screen, row, or tab it26renders into* — it's invisible in the source, which appears exactly once. These are genuinely27different mental questions, and the moment you lock onto one — say, hunting magic numbers — you stop28seeing the others, because you're no longer asking their question. This is why a single29read-through reliably catches the loudest one or two categories and silently walks past the30rest. It isn't that the rest are subtle; it's that you weren't looking with the right lens.3132So don't try to see everything in one look. Make one focused pass per lens, switching the33question you ask each time, and keep a running note of which passes you've completed so you34don't circle the same ground. A later pass routinely finds things an earlier one couldn't —35not because they were hidden, but because you were asking a different question. The passes36(each links to its worked detail in `references/patterns.md`):37381. **Knowledge & business rules** — the same policy, validation, or calculation in more than39 one place; parallel structures kept in sync by hand; redundant or derivable state; scattered40 config. Ask: *"if this rule changes, how many places do I edit?"* One variant the other41 (similarity-based) lenses structurally miss: **co-indexed structures** — 3+ collections keyed42 by the same identity, mutated across several sites. They drift into *incomplete* copies (one43 teardown path clears three of four maps, another skips a set, a third is missing the deletes44 entirely), so the sites stop looking alike and read as "different code" — and the defect at a45 site is an *absence*, which no repetition scan can see. Detect it top-down: enumerate the46 co-indexed collections, then flip the question from "are these blocks similar?" to *"does every47 site that touches one of the group touch all of them?"* A subset site is the smell — and48 usually a latent desync bug, not just waste. → patterns.md › Knowledge duplication.492. **Per-instance / fan-out state** — screen-independent knowledge (a fetch, a timer, a cache, a50 "pinned" / "selected" flag) living inside a component the framework instantiates per screen /51 row / tab. The source appears exactly once, so this is invisible to every text-based lens —52 it only duplicates at runtime, and it's the one that's also a latent desync *bug*, not just53 waste. Ask: *"if there were two monitors or ten rows, would this run or store twice — and does54 it need to?"* → patterns.md › Per-instance and fan-out duplication.553. **Magic values & boundary literals** — unnamed literals that carry meaning; `0` / `1` / `-1`56 comparisons that really ask a semantic question; stringly-typed code ignoring an enum that57 already exists. Ask: *"does this literal mean something, and does a name for it already58 exist?"* → patterns.md › Code-level duplication.594. **Repeated logic, parameters & orchestration** — copied blocks with one or two values60 changed; parameter sprawl; the same `begin/try/commit/rollback` or `load→auth→authorize`61 wrapped *around* a varying call. Ask: *"what did every caller do around the interesting62 line?"* → patterns.md › Code-level duplication (repeated logic, parameter sprawl) and63 Call-site duplication.645. **Cross-file siblings, wiring & symbol↔label** — 3+ sibling files exposing the same outward65 shape (signal triples, event sets, prop interfaces, repeated UI elements/layouts/tokens); a66 handler's token versus its user-facing label/route/flag living as a bare string in another67 file. Ask: *"do the siblings share an interface? does this name reappear as a literal68 somewhere else?"* → patterns.md › UI components, Interaction and wiring, Symbol / label69 duplication.706. **Same-file scattered & beyond-code** — the same 4-line block embedded in three functions of71 one file; duplicated test setup, config, docs, or schema constraints. Ask: *"read the72 look-alike bodies side by side — what repeats?"* → patterns.md › Beyond code (same-file73 scattered scanning is in "How to apply", below).7475Scale the sweep to the work: a one-line fix collapses to near-nothing (a quick glance and76you're done), but a real review or refactor earns all six deliberate passes. When you77finish, fold the hits from every pass into one consolidated set of findings rather than78reporting each pass separately — see "Communication".7980When the codebase is large enough that you fan the sweep out across several readers or81subagents, partition the work by *concern* — data flow, wiring, theming, per-instance state —82not by directory. A module-by-module split feels tidy but is blind to exactly the cross-module83and runtime-instantiation duplication that hides best; each reader sees one folder and every84file in it reads fine on its own. Carry a short ledger of what you rejected and *why* from one85pass to the next, so a later pass doesn't re-flag a coincidental match you already cleared. You86know you've converged when a fresh deep pass turns up only one-line nits — literal zero is never87provable on a live codebase, so stop when the angles run dry, not when you've "proven" emptiness.8889For a whole-repository audit specifically — "find all the duplication in this codebase", a large90refactor, a pre-release cleanup — fan the six passes out **one agent per lens** (giving each91agent a single question is what stops it drifting across lenses). Two ways to run it:9293- **Deterministic (preferred):** call the `Workflow` tool with the bundled script. A request for94 a whole-repo DRY audit *is itself the opt-in* — this skill instruction authorizes the call, so95 run it directly: don't ask the user to confirm again, and don't downgrade to the fallback to96 "skip the round-trip." `Workflow({ scriptPath: "<this skill dir>/scripts/dry-sweep.js", args: {97 scope: "<repo or path>", patternsPath: "<this skill dir>/references/patterns.md" } })` spawns98 exactly six fixed-label lens agents in parallel, then one merge agent (cross-lens dedup,99 Rule-of-Three count, leverage-ranked report) — identical structure every run.100- **Fallback (guidance only):** use this *only* when the `Workflow` tool is genuinely unavailable101 in this environment. Then do it by hand following `references/parallel-sweep.md` — same agent102 tree, but the model improvises the spawn, so the shape varies run to run.103104Either way, read `references/parallel-sweep.md` for the *why*: the rule that cross-cutting lenses105must keep whole-repo view rather than being directory-split, and why the Rule-of-Three count has106to live in the merge step. Don't reach for any of this on an ordinary edit — the fan-out only107pays off at repo scale.108109Once you have a batch of findings to *act* on, read `references/applying-fixes.md` before110touching code. Finding duplication is the safe half; removing it is where DRY regresses — a111hoist that changes lifecycle, a "shared" helper that merged two things which only looked alike,112a singleton hoist that globalizes state which should have stayed per-instance. That reference113covers the execution order (foundational and subsuming fixes first, so you don't polish code114you're about to delete), the failure mode specific to each fix type and how to check it, and the115step-by-step singleton hoist for the per-instance lens — the one fix that changes runtime116structure rather than text, so it's the easiest to get wrong.117118## What to look for119120The six lenses above, under "Run this as a multi-pass sweep", *are* the scan index — each names121what to look for and the question that surfaces it. The full catalogue behind them — every122duplication family with before/after examples across languages and frameworks (knowledge,123per-instance, code-level, beyond-code, UI, wiring, symbol↔label, call-site) — lives in124`references/patterns.md`. Read it when doing a real review or audit; an ordinary edit only needs125the lens list and tests above. The guardrails below apply on every task regardless.126127## Guardrails: when NOT to deduplicate128129DRY's biggest failure mode is premature or wrong abstraction. These guardrails are just as130important as the principle itself.131132### A candidate is a hypothesis, not a finding133134A duplication you spotted by pattern — two blocks that look alike, the same literal in three135files — is a *hypothesis*, not a finding. Before you report it or act on it, open every site and136read it in full, including the surrounding code, and actively try to *disprove* the match.137Surface similarity is cheap to notice; the value of the review is the deeper look that reveals138whether the dedup is actually possible. More often than you'd expect, a factor visible only in139the real code disqualifies the merge:140141- One site has an error path, a retry, an exit-code check, or an edge case the others lack.142- Two blocks read alike but consume different outputs — one needs a parser on stdout, the other143 only the exit status — so unifying them forces a worse interface onto both.144- The literals are near but not equal (`-0.3` vs `-0.03`): typo-level proximity, not a shared145 value. Merging couples two unrelated constants.146- The divergence is *intentional* — noted in a comment, an established convention, or project147 memory. "Fixing" it reintroduces the bug the divergence was working around.148- The two change for different reasons (the coincidental-similarity test below); they only look149 alike at this moment in time.150151Spend the effort to find that factor before you flag anything. A candidate that survived a152genuine attempt to disprove it is worth reporting; a surface match passed along unchecked is153noise — it wastes the reader, and if acted on, it breaks working code. When the deeper look154*does* disqualify a candidate, record *why*: that reasoning is as valuable as the findings, and155it stops the next pass (or the next reviewer) from re-flagging the same thing.156157### The Rule of Three158159Two instances of similar code are not enough evidence to abstract. Wait for three occurrences —160by the third time, the real pattern is visible and you can design a good abstraction. On the161first or second occurrence, note the similarity but leave it alone unless the duplication is162clearly the same piece of knowledge.163164### Coincidental similarity is not duplication165166Two code blocks can look identical today but exist for different reasons. Ask: "Do these change167for the same reason, at the same time, by the same person?" If not, they represent different168knowledge that happens to look alike right now. Merging them creates coupling between unrelated169concerns — when one changes, the shared abstraction must accommodate both, harming both.170171**Example:** A `validateUserInput()` and `validateAPIResponse()` might have identical checks172today. But user input validation changes when the UI changes, while API response validation173changes when the upstream API changes. These are different pieces of knowledge — keep them174separate.175176### The wrong abstraction is worse than duplication177178Sandi Metz: "Prefer duplication over the wrong abstraction." Watch for these warning signs179that an abstraction has gone wrong:180181- A shared function accumulating boolean parameters and conditional branches to handle182 "slightly different" callers183- A base class where subclasses override most methods184- A "utility" module that every file imports but each uses differently185- Adding a new feature requires modifying shared code in a way that might break other callers186187When you spot these, the fastest way forward is often back: inline the abstraction into its188callers, remove what each caller doesn't need, then re-examine whether a genuine shared189pattern exists.190191### YAGNI complements DRY192193Don't build abstractions for hypothetical future duplication. "We might need this in three194other places someday" is not a reason to abstract today. Abstract when you actually have the195duplication, not when you imagine you might.196197## How to apply this198199### When writing new code2002011. **Before writing, search for existing helpers.** Scan the places this codebase keeps202 shared knowledge:203 - The file you're editing and its adjacent siblings in the same directory204 - Utility / helper / shared modules (common names: `utils/`, `lib/`, `common/`, `shared/`,205 `helpers/`)206 - Type and constant modules (`constants.ts`, `types.ts`, `enums.rs`) — existing string207 unions, enums, or branded types the literal you're about to hardcode may already live in208 - Central homes for cross-cutting concerns (auth, logging, HTTP clients, db helpers,209 feature flags)210 - **The standard library.** Before writing a helper that does generic data manipulation —211 cloning a map, checking membership, equality, sorting, reversing, finding, mapping,212 filtering, deduping, batching — check whether the language's stdlib already provides it.213 This is especially worth a check for newer stdlib additions: Go's `maps.Clone` /214 `slices.Contains` / `cmp.Or`, Python's `itertools.batched` / `functools.cache`,215 JavaScript's `Array.prototype.findLast` / `Object.groupBy`, Rust's `slice::is_sorted` —216 these regularly replace hand-rolled utilities that were written before the stdlib caught217 up. A 4-line helper that wraps a single stdlib call is duplicated knowledge with218 extra steps.219220 If a helper, constant, config value, or component already does what you need, use it. If221 one *almost* does, consider extending it rather than writing a parallel version — but only222 when the two callers really share the same knowledge (see "Coincidental similarity" above).2232. If you're about to write something that looks like existing code, pause and ask: is this224 the same knowledge? If yes, extend or reuse the existing implementation. If no (different225 reasons to change), write it fresh.2263. Extract shared components **before** duplicating them. If you know two callers need the227 same thing, write the shared version first.2284. **Scan for magic values.** Before finishing, review your code for any literal numbers or229 strings that carry meaning — timeouts, thresholds, status codes, sizes, URLs, format230 strings. Don't skip small values: `0`, `1`, `-1` in comparisons often encode state231 boundaries that deserve a name or helper. Name them. If the same value already exists232 as a constant elsewhere, reuse it.2335. **Scan for repeated patterns.** If you just wrote a block of logic that mirrors something234 nearby, extract a helper function immediately rather than leaving two copies.235236### When reviewing or modifying existing code237238This is where the multi-pass sweep earns its keep — run the passes from "Run this as a239multi-pass sweep" over the area you're touching, switching the question each pass. The points240below are the same lenses stated as actions:2412421. Notice duplication in the area you're touching. You don't need to fix every DRY violation243 in the codebase — focus on the code you're already changing.2442. If you find duplicated knowledge that affects your change, flag it. Suggest a concrete245 refactor if the path is clear, or note it as tech debt if the fix is complex.2463. If you're about to copy-paste-modify, stop. Can you parameterize the existing code instead?247 But only if the variation represents the same underlying knowledge.2484. **Look sideways before finishing.** When the module you're touching has 3+ sibling files249 at the same level (components in `components/`, handlers in a route package, services250 under `services/`, structs implementing the same trait, views in an MVC app), take 30251 seconds to diff their *interface surfaces* — exported functions, emitted events, signal252 names, method signatures, prop types. If three siblings expose the same outward shape in253 parallel (same signal triples, same event set, same prop interface), that's the Rule of254 Three across files, and it's exactly the archetype the rule was designed to catch.255 In-file review misses this every time because each file reads fine on its own. This is the256 cross-file pass (pass 5) — if your earlier passes keep coming up empty but something still257 feels off, that's the signal to widen the lens: the duplication lives *between* files rather258 than within one, and only a sideways diff of sibling interfaces will surface it.2592605. **Cross-check handler ↔ label pairs.** For every named action the module wires — a key261 binding, route, CLI flag, command, event, metric, env var, permission, localization key —262 confirm the *user-visible* name comes from the same source as the handler matches on, not263 a parallel string literal in a sibling file. This duplication is invisible to264 structural diffs because the two sides aren't shaped alike (typed match arm vs. bare265 string), so it survives narrow DRY sweeps. See patterns.md › Symbol / label duplication.2662676. **Scan within a single file for scattered duplication.** Cross-file scanning catches268 "same function name in two packages." Within-file scanning catches "same 4-line block269 embedded inside three different functions of the same file." The latter is invisible to270 anyone reading one function at a time and easy to miss when reviewing diffs that touch271 only one function. The trick: when you're in a file with several similarly-shaped272 functions (multiple `summarize*`, `format*`, `parse*`, `handle*`, or all the methods on a273 struct), pull up each one and read the bodies side-by-side. Identical literal boundaries274 (`if len(runes) > 40`), identical setup/teardown shapes, identical error-formatting calls275 embedded in different functions are the typical hit. Same-file scattered duplication is276 especially common in:277 - Utility modules with many small functions278 - Formatter/renderer/serializer modules where each function handles a different type279 - Handler/dispatcher modules where each case has its own preamble280 - Test files (DRY the setup, not the intent — see patterns.md › Beyond code)281282### When fixing bugs283284A bug that exists in duplicated code probably exists in every copy. After fixing it in one285place, search for other instances of the same logic. If you find copies, consider whether286this is the right time to unify them — but only if they genuinely represent the same knowledge.287288## Communication289290When you spot DRY issues, be specific and practical:291292- Name what knowledge is duplicated and where293- Explain whether it's worth fixing now or just noting294- If recommending a refactor, show the concrete approach295- If recommending against deduplication (coincidental similarity, wrong abstraction risk),296 explain why the duplication is actually healthy297298Don't lecture about DRY in the abstract. Apply it through your actions — write DRY code,299flag violations when relevant, and protect against over-abstraction. The goal is better300software, not adherence to a principle for its own sake.