DRY and YAGNI Explained: 2 Rules That Save Your Code (and Sanity)
Last Thursday I watched a junior developer on my team spend three hours building a generic "notification engine" that could route alerts via email, SMS, Slack, and carrier pigeon. We only needed email. The spec never mentioned the other channels. The developer was trying to be clever — future-proofing, they said. By Friday we had 400 lines of untested switch statements and zero working emails. That afternoon I sat them down and explained two acronyms that would have saved those three hours: DRY and YAGNI. These aren't just buzzwords engineers throw around to sound smart. They are lifeguards for your codebase. DRY (Don't Repeat Yourself) stops you from drowning in duplicated logic. YAGNI (You Ain't Gonna Need It) stops you from building a swimming pool when all you need is a bucket. Together they keep your code lean, your deadlines sane, and your future self out of therapy.
What DRY Really Means (And What It Doesn't)
DRY stands for "Don't Repeat Yourself." The principle comes from Andy Hunt and Dave Thomas's book The Pragmatic Programmer. At its core, DRY says: every piece of knowledge should have a single, unambiguous representation within a system. That sounds simple, but most people get it wrong. They think DRY means "never copy-paste code." That's a misunderstanding. DRY isn't about avoiding repetitive characters — it's about avoiding repetitive knowledge.
Let me give you a concrete example. Say you have two functions that calculate tax for different product types. One calculates sales tax for physical goods. Another calculates VAT for digital services. Both formulas look similar — multiply a rate by a base price. You might be tempted to abstract them into a single calculateTax(rate, price) function. That's fine if the two calculations share the same business logic. But what if sales tax has a cap of $200 and VAT has a floor of $5? Now your shared function needs conditional branches. The knowledge of each tax rule gets tangled together. When the tax law changes for one product type, you have to untangle the whole mess. That's the DRY trap.
True DRY is about ensuring that a change in business logic requires a change in exactly one place. If two pieces of code do the same thing for the same reason, consolidate them. If they happen to look the same but exist for different reasons, keep them separate. The code itself is cheap to write. The knowledge duplication is what costs you.
A Real-World DRY Trap – When Reusing Code Backfires
I once inherited a codebase where a previous developer had abstracted every database query into a single "universal repository" class. Every read, write, and join went through the same generic method. It was DRY on steroids. The problem? The generic method had a dozen optional parameters — where, orderBy, limit, joinTables, groupBy, having, distinct — most of which were null for any given call. Reading a simple user record required scrolling through 80 lines of conditional SQL concatenation. Adding a new feature meant praying you didn't break an unrelated query. The original developer had achieved zero code duplication but at the cost of extreme complexity. That's not DRY. That's over-abstraction. The fix was to break that monolith into purpose-built query methods — each one small, specific, and easy to test. The total lines of code increased, but the cognitive load dropped. Sometimes a little duplication is cheaper than a tangled abstraction.
YAGNI – The Art of Not Building What You Don't Need Yet
YAGNI stands for "You Ain't Gonna Need It." It originated in Extreme Programming, specifically from Ward Cunningham and Ron Jeffries. The principle is brutally simple: only build what you need right now. Don't add features, infrastructure, or abstractions based on what you think you might need in the future. The future is uncertain. The code you write today for a hypothetical tomorrow often becomes dead weight — or worse, gets in the way of the actual requirement that eventually emerges.
I see YAGNI violations all the time. A developer adds caching to a new endpoint "because it might get slow later." They build a configurable dashboard because "the client might want different metrics next quarter." They write a generic plugin system because "we'll probably integrate with other tools someday." Each of these decisions adds complexity, testing overhead, and maintenance cost — for a future that may never arrive.
Here's a simple litmus test for YAGNI: ask yourself, "If I don't build this now, will the software work correctly for the current requirements?" If the answer is yes, don't build it. If the answer is no, then it's a real need — build it simply. You can always add flexibility later when the actual need materializes.
The Cost of Ignoring YAGNI: A Tale of Unused Features
A few years ago, a team I consulted with spent six weeks building a highly configurable analytics dashboard. Users could customize charts, save views, set up email reports, and configure alerts. It was beautiful. It was also never used. The client's actual need was a single static table of numbers that updated once a day. They never opened the dashboard settings. They never configured an alert. The team had over-engineered for a fantasy future. Those six weeks could have been spent on three other features the client actually requested. The dashboard became a maintenance burden — every framework upgrade required updating the custom chart library. Eventually they ripped it out and replaced it with a CSV export. Sometimes the simplest solution is the right one.
How DRY and YAGNI Work Together (Without Contradicting Each Other)
On the surface, DRY and YAGNI seem to pull in opposite directions. DRY says "don't repeat knowledge." YAGNI says "don't build for the future." If you apply them blindly, you might create a conflict: DRY wants you to abstract early, but YAGNI tells you to wait. The resolution is simpler than it looks. The two principles operate on different axes. DRY governs how you structure existing knowledge. YAGNI governs what knowledge you commit to code in the first place.
Here's the balanced approach I've used for years: let YAGNI be your gatekeeper for new features. Build the simplest thing that works for the current requirement. If that introduces some duplication, that's acceptable — temporary duplication is better than premature abstraction. Then, when you see the same pattern or logic for the third time, apply DRY. This is known as the "rule of three." Two occurrences might be coincidence. Three is a pattern worth consolidating. By waiting, you have more context about the real shape of the shared logic. Your abstraction will be better because you've seen the pattern repeat in practice, not just in your head.
Think of it as a decision flow: start with a new feature. Ask YAGNI: "Do I need this for the current requirement?" If no, drop it. If yes, build it simply. Now you have some code. A few weeks later, a similar need appears. You copy-paste and modify slightly. That's fine. A third time appears. Now you have three concrete examples. You can see the common core and the variations. That's the moment to extract the shared logic into a single representation — true DRY. The sequence matters: YAGNI first, then DRY after the pattern proves itself.
Practical Checklist – Applying DRY and YAGNI in Your Next Project
Here's a checklist I keep pinned to my desk. It has saved me from dozens of rabbit holes:
- Start with YAGNI for every new feature. Write the minimum code that satisfies the requirement. No caching, no configurability, no plugin hooks — unless the requirement explicitly asks for them.
- Allow temporary duplication. When you encounter a similar task, it's okay to copy-paste the first time. Mark it with a TODO comment:
// TODO: consider DRY after third occurrence. This gives you permission to move fast now and refactor later. - Apply the rule of three. When you see the same pattern for the third time, stop and refactor. Extract the shared logic into a single function, class, or module. Make sure the abstraction truly captures the common knowledge — not just similar-looking code.
- Use code reviews to catch violations. During code review, flag two things: premature abstraction (likely a YAGNI violation) and unnecessary duplication that has hit three instances (a DRY opportunity). A good reviewer will ask: "Why is this abstracted already?" or "This is the third time I've seen this logic — want to consolidate it?"
- Keep it local. When you do DRY, keep the shared code close to where it's used. Don't put it in a global utility library until you're certain it's genuinely reusable. A function used by two modules in the same folder should live in that folder, not in
src/shared/utils/genericHelpers.ts. - Document the decision. When you choose to duplicate instead of abstract, or abstract instead of duplicate, leave a brief comment explaining why. Future you (or a teammate) will thank you for the context.
These two principles have saved me more times than I can count. They don't guarantee perfect code, but they prevent the two biggest wastes of time in software development: duplicating knowledge that should be shared, and building things that should never have been built. The next time you're about to write a generic notification engine for a feature that only needs email, stop. Remember YAGNI. Build the email. Ship it. Then, if three different features need email, maybe then you abstract. Your future self — and your teammates — will sleep better.