Copy, Paste, Regret: The Hidden Cost of Repeating Yourself in Code
Why duplicated code is a ticking time bomb: what DRY actually means, how "identical" copies quietly diverge into different behavior, and the one caveat that keeps DRY from becoming its own disaster.

On Friday you fixed the bug. The fix was reviewed, tested, merged, and deployed. On Monday the same bug is back. Same symptom, same bad output, same angry ticket. Except it isn’t back, because it never left. The logic you fixed existed in two places, and you only found one of them.
If that story makes your stomach drop a little, you’ve lived it. This post covers why it keeps happening: what DRY actually means (it’s subtler than the slogan), what duplication really costs, and the caveat that keeps DRY from becoming its own disaster. Then we’ll finish with what DRY looks like when an LLM assistant writes the first draft, and how to stop that assistant from becoming the fastest duplication machine ever built.
What DRY actually says (it’s not “never repeat a line”)
DRY stands for Don’t Repeat Yourself, coined by Andy Hunt and Dave Thomas in The Pragmatic Programmer. Their original phrasing is more precise than the folklore version:
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
The key word is knowledge, not code. DRY is about facts and rules: how a discount is calculated, what makes an email valid, how many days before a session expires. Write one of those facts down in more than one place and the system no longer has one answer to the question. It has several, and they only agree by coincidence.
Coincidence has a short shelf life in software.
What violations look like in the wild
1. The duplicated rule
A signup validator and a profile-update validator, written eight months apart by two different people:
// SignupValidator.java
public class SignupValidator {
private static final Pattern EMAIL =
Pattern.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$");
public List<String> validate(SignupInput input) {
List<String> errors = new ArrayList<>();
if (!EMAIL.matcher(input.email()).matches()) {
errors.add("Invalid email address");
}
if (input.password().length() < 8) {
errors.add("Password must be at least 8 characters");
}
return errors;
}
}
// ProfileUpdateValidator.java
public class ProfileUpdateValidator {
private static final Pattern EMAIL =
Pattern.compile("^[^\\s@]+@[^\\s@]+$"); // <-- look closely
public List<String> validate(ProfileInput input) {
List<String> errors = new ArrayList<>();
if (!EMAIL.matcher(input.email()).matches()) {
errors.add("Invalid email address");
}
return errors;
}
}
The two email patterns are different. One requires a dot in the domain and the other doesn’t. Neither author decided this; it’s an accident of copy, paste, and tweak. A user can now sign up under one rule and update their way into a state signup would have rejected. When someone eventually “fixes email validation,” they’ll fix whichever copy shows up first in search. The other keeps shipping the old behavior indefinitely.
That’s the core failure mode of duplication: fixes don’t propagate. Every copy is a place a future fix can miss.
2. The duplicated fact
// checkout/CartService.java
private static final BigDecimal FREE_SHIPPING_THRESHOLD = new BigDecimal("50");
// email/OrderConfirmationEmail.java
String shippingNote = total.compareTo(new BigDecimal("50")) >= 0
? "Free shipping!"
: "Shipping: $4.99";
// admin/ShippingReport.java
List<Order> freeShippingOrders = orders.stream()
.filter(o -> o.total().compareTo(new BigDecimal("45")) >= 0) // "close enough"
.toList();
One business fact, “orders over $50 ship free,” written three times: a named constant, a magic number, and a wrong value. Or is the report intentional? Nobody remembers. When marketing moves the threshold to $60, someone updates the checkout, the email keeps promising free shipping at $50, and the finance report was quietly measuring something else all along.
The bugs are bad enough, but the worse part is that you can no longer answer a basic question about your own system. What is our free-shipping threshold? That should have one answer. Here it has three.
3. The security patch that misses a copy
This one earns its own entry because the stakes are different. Avatar uploads got a file-type check years ago, and when support-ticket attachments were built the check came along for the ride:
// AvatarUploadController.java (also pasted into TicketAttachmentController.java)
private static final Set<String> BLOCKED = Set.of("exe", "bat", "sh", "jar");
void requireSafe(String filename) {
String ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
if (BLOCKED.contains(ext)) {
throw new UnsupportedFileTypeException(ext);
}
}
A researcher reports the design flaw: it’s a denylist, and denylists lose. .html isn’t blocked, so an uploaded HTML file gets served back from your domain as a ready-made phishing page. The team fixes it properly with an allowlist:
// The fix, applied to the avatar upload
private static final Set<String> ALLOWED = Set.of("png", "jpg", "jpeg", "gif", "pdf");
The avatar endpoint is patched. The pasted check in TicketAttachmentController still runs the old denylist. Same file, same attack: rejected on your profile page, accepted in a support ticket.
And here’s the uncomfortable part: shipping the fix also published the vulnerability. The advisory (“uploads are now restricted to an approved list of file types”) announces exactly what used to be possible. An attacker just takes the file your profile page rejected and feeds it to every other upload button you have. One still says yes.
Duplication turns one vulnerability into a scavenger hunt, and attackers only need to find the one copy you missed.
Why this is worse than it looks
Each example costs more than the obvious “you fix things twice”:
- Divergence is silent. No test fails when copies drift apart; each copy is individually “working.” The inconsistency stays invisible until a user walks through the gap.
- Review surface inflates. Reviewers approve a change to a validation class without knowing three siblings exist.
- Bugs get fixed probabilistically. A fix lands in however many copies the author happened to find.
grepis your deployment mechanism, andgrepmisses renamed, reformatted, and rewritten copies (“the mobile app does its own version in Kotlin”). - Onboarding turns into archaeology. A new engineer finds both copies, can’t tell whether the difference is a bug or a requirement, and guesses. Now the divergence has defenders.
The honest caveat: DRY has a failure mode too
One warning before you go deduplicate everything, because over-applying DRY causes its own outages.
Two pieces of code can be textually identical but represent different knowledge. A Customer validation and a Supplier validation might both say “name required, email required” today, but they’ll change for different reasons, on different schedules, for different stakeholders. Merge them and you’ve coupled two concepts that only rhyme by coincidence. When they need to diverge again (they will), someone forks the shared class back apart or, far worse, starts adding flags:
validator.validate(entity, /* isSupplier */ true, /* skipEmailCheck */ legacyMode);
Sandi Metz put it best: duplication is far cheaper than the wrong abstraction. So the question to ask about two similar blocks is whether they encode the same fact about the domain, not whether their text happens to match. If updating one copy without the other would always be a bug, unify them. If they could legitimately need to differ someday, leave them alone. The “rule of three,” waiting for a third copy before abstracting, is a decent heuristic when you’re not sure.
With that guardrail, DRY becomes what it was always meant to be: a judgment call about where knowledge should live.
DRY in the LLM world
On most teams today, much of the new code is drafted by a coding assistant, and that changes the duplication math. Not in your favor.
An LLM doesn’t know your codebase already has a canonical EmailValidator unless something shows it. Ask for “a method that validates emails” and you’ll get a fresh regex, subtly different from the three you already have. Generating plausible code from scratch is the model’s default mode, and every generation is a brand-new copy. It is the fastest copy-paste-and-tweak machine ever built, and it never gets bored.
The answer isn’t to stop using the tools. Prompt and configure them the way you’d onboard a new engineer who hasn’t seen the codebase yet.
Make reuse part of the request.
❌ Don’t prompt like this:
Add email validation to the ticket form.
✅ Do this instead:
Add email validation to the ticket form. First search the codebase
for existing email validation. If something exists, call it. If not,
create it in commons/validation and call it from there.
The first invites a fourth copy. The second produces either a reuse or a new single home for the rule. Agentic tools can search your repository; make them do it before they write.
Put your conventions where the model reads them. Most assistants read a project instruction file (CLAUDE.md, AGENTS.md, or your editor’s rules file). That’s the one place your DRY policy reaches every generation:
- Before writing a new helper, search for an existing one first.
- Business rules and constants live in one place. Import them;
never inline a threshold, fee, or limit.
- If you find two implementations of the same rule, stop and flag
it instead of silently picking one.
Review generated code for duplication specifically. Review of AI output tends to ask “does it work?” Add a second question: “does this knowledge already have a home?” If the assistant hands you forty lines that feel strangely familiar, run grep before you commit.
All three habits come back to the question The Pragmatic Programmer asked back in 1999: is this the same knowledge, or does it just look similar? Your assistant can draft the code, but it can’t answer that for you, and it gets the answer wrong much faster than you would.
The takeaway
DRY means every fact in your system has exactly one home, so changes and fixes have exactly one place to go. Duplication breaks that quietly. No test fails on the day the second copy is pasted; the bill arrives later, as the Friday fix that doesn’t survive the weekend.
Caught early, duplication is cheap to fix. The moment you reach for copy-paste, ask: same knowledge, or just similar-looking? Same fact: extract it now, while the copies are still identical. It will never be easier. Different knowledge that merely rhymes: paste with a clear conscience.
In practice, though, you’ll usually inherit duplication rather than catch it fresh: pasted years ago, drifting since, and nobody left who remembers which differences are load-bearing. Untangling that safely is a craft of its own, and it deserves more than a closing paragraph.
Read next: Diff, Merge, Breathe: How to Safely Unify Code Copies That Drifted Apart
The sequel: the copies are 70% identical and you don’t know which parts of the other 30% keep the lights on. Find every copy, separate bugs from requirements, pin behavior with tests, and merge without breaking production.