Back to Blog
Opinion AI Architecture Craft CMS DRY

The Case Against DRY in the Agentic Age

· 9 min read

I'm going to say something that would have gotten me side-eyed in a code review five years ago: a lot of the abstractions we build in the name of DRY are making our codebases worse. Not a little worse. Worse in a way that gets more expensive every month, now that agents write most of the code we ship.

"Don't Repeat Yourself" is one of the first rules most of us ever learned. It's on the whiteboard in every bootcamp. It's the reflex that fires the second you see two blocks of code that look alike. And for a long time it was mostly good advice, because the thing it protected was the scarcest resource on the team: a human being's time and attention.

That resource isn't the bottleneck it used to be. So I think it's time we revisit the rule. I'm proposing a replacement, and yes, I gave it an acronym, because that's apparently how you get developers to take an idea seriously.

Meet RYAN: Repeat Yourself As Needed

RYAN stands for Repeat Yourself As Needed. It doesn't mean "copy and paste everything forever." It means duplication is a tool, not a sin. You reach for an abstraction when it earns its place, not because two things happen to look similar on a Tuesday.

The short version:

  • Repeat code freely. Two templates that render similar markup can just be two templates.
  • Don't repeat knowledge. A business rule, a tax calculation or a permission check should live in exactly one place.
  • Abstract when it hurts, not when it's possible. Wait until the duplication is actually causing bugs or real maintenance pain, and you understand the shape of the thing well enough to name it.

If that sounds familiar, it should. Sandi Metz said years ago that duplication is far cheaper than the wrong abstraction. Kent C. Dodds called it AHA, "Avoid Hasty Abstractions." None of this is new. What's new is that the economics behind it changed, and they changed fast.

DRY Was Never About Typing

Here's the part that gets lost. When Andy Hunt and Dave Thomas coined DRY in The Pragmatic Programmer, they defined it as "every piece of knowledge must have a single, unambiguous, authoritative representation within a system." Knowledge. Not characters. Not lines that look the same.

Somewhere along the way the industry turned that into "if two chunks of code look alike, merge them." Those are very different rules. Two blocks of code can look identical today and represent completely different ideas that just haven't drifted apart yet. Merge them and you've welded two unrelated things together. Now every change to one is a negotiation with the other.

We did this for years anyway, and I understand why. When a human had to find and update every copy by hand, duplication had a real, obvious cost. You'd fix a bug in one place, miss the other three, and ship it. An abstraction was insurance against your own forgetfulness.

What Changed

Most of the code I ship now is written by an agent. I review it, I steer it, I throw a fair bit of it away, but the typing and a lot of the searching is done by something that doesn't get tired or bored. And that flips two costs that DRY was built around.

The cost of duplication went way down

The classic argument for DRY is "if you need to change it, you'll have to change it in five places." Fine. An agent can grep a codebase and update five places in about the time it takes me to refill my coffee. It doesn't forget the fourth file because it got distracted by Slack. Finding every copy of a pattern and changing it consistently is exactly the kind of work these tools are good at.

I'm not saying duplication is free. It isn't. But "I might miss one when I update it" used to be the dominant cost, and it isn't anymore.

The cost of abstraction went up

Meanwhile, abstractions got more expensive. Every layer of indirection is something an agent has to load, read and hold in context before it can safely make a change. A partial that includes a partial that calls a macro that reads a config array is four files the agent has to understand to change one button. Sometimes it does. Sometimes it guesses, and the guess looks plausible, and that's worse.

Agents do their best work when the thing they're changing is right in front of them. Locality beats cleverness. A self-contained template that says exactly what it renders is easy to reason about for a model and for me when I review the diff. A "flexible" shared component with nine boolean parameters is not easy for anybody.

The blast radius is bigger

This is the one that actually bites. When an agent tweaks a shared helper to fix one page, every other caller of that helper just changed too. The diff is three lines. The impact is fourteen templates, and nothing in the diff tells you that. A human who wrote the helper might remember who else uses it. An agent working a single ticket usually doesn't go looking unless you tell it to.

Duplicated code has a small blast radius by design. Change the blog card and you changed the blog card. That's a feature.

What This Looks Like in a Craft Project

You've probably seen this one. It starts as a nice reusable card partial. Then the case studies need a slightly different card. Then the team page. Then the events listing, but only on mobile, and only when there's no image. A year later the include looks like this:

templates/_components/card.twig usage

{% include '_components/card' with {
    entry: entry,
    variant: 'compact',
    showDate: true,
    showExcerpt: not isMobile,
    hideImage: entry.image is empty,
    imageRatio: variant == 'compact' ? '1:1' : '16:9',
    ctaLabel: entry.type.handle == 'event' ? 'Register' : null,
    headingLevel: 3,
    theme: 'dark',
} only %}

And inside card.twig is a pile of conditionals that nobody fully understands, including whoever wrote it. Every new use case adds a flag. No flag ever gets removed, because who knows what's using it.

Now ask an agent to "make the event cards show the venue." It has to read the partial, trace every flag, figure out which combination is the event card, add a new condition, and hope it didn't change the case study card on the way through. Compare that to this:

templates/events/_card.twig

<article class="event-card">
    <h3>{{ entry.title }}</h3>
    <p class="event-card__meta">
        {{ entry.eventDate|date('M j, Y') }} · {{ entry.venue }}
    </p>
    <a href="{{ entry.url }}">Register</a>
</article>

Yes, it overlaps with the blog card. Maybe forty percent of the markup is the same. Who cares? It's six lines, it says exactly what it does, and changing it can't break anything else on the site. An agent will get that change right on the first try, and I can review it in five seconds.

Same story on the PHP side. I've inherited plenty of custom modules with an abstract base service, a trait or two, and a "generic" sync class that handles three different APIs through a config array. Each API ended up with its own special cases, so the generic class is really three classes pretending to be one. Splitting them into three plain services, each a bit repetitive, has made them easier to change every single time.

I run into this in my own work too. I maintain a lot of Craft plugins, and most of their marketing sites started life as separate installs. Plenty of those templates are close copies of each other. A few years ago that would have bugged me. Now, when I need to change one, I point an agent at it and the change stays in that one site. The duplication hasn't cost me anything I can point to.

But What About Consistency?

This is the fair objection, and I want to take it seriously. If you have eight card templates, won't they drift? Won't the spacing on one be a little off? Won't someone fix an accessibility bug in one and not the others?

Sometimes, yes. Here's how I think about it:

  • Put the consistency in the design system, not in the templates. Your spacing, type scale and colors belong in Tailwind theme tokens or CSS variables. That's shared knowledge, and it should absolutely be DRY. The markup that uses those tokens can repeat.
  • Make fixing all copies a normal request. "Fix the focus state on every card template" is a perfectly good prompt. Agents are good at sweeping changes when you ask for them explicitly.
  • Let the tests and the checks hold the line. Accessibility linting, visual regression tests, a script that asserts every card has an alt attribute. Those catch drift far more reliably than a shared partial ever did.

A useful gut check: if the two things would change for different reasons, they're different things, even if the code looks identical today. Keep them apart.

When DRY Still Wins

I'm not arguing for chaos. There's a real list of things you should never duplicate, agent or no agent:

  • Business rules. Pricing, discounts, tax, shipping thresholds, eligibility. If the rule lives in two places, one of them is eventually wrong, and that one ends up costing somebody money.
  • Security and permissions. Access checks, sanitizing, CSRF handling, auth logic. One place. Always.
  • Configuration and secrets. Environment config, API endpoints, feature flags.
  • Data contracts. The shape of an API response or a JSON feed that other systems consume. Two definitions of the same contract is how you get integrations that fail quietly.
  • Design tokens. As mentioned above. Colors and spacing are knowledge.

Notice what all of those have in common. They're knowledge, not markup. That's the original DRY, the one Hunt and Thomas actually wrote down. RYAN isn't really a rejection of DRY. It's a rejection of the watered-down version we've been practicing, where looking the same counts as being the same.

The Uncomfortable Part

Here's where I expect people to push back hardest. A lot of the abstractions we build aren't really for the code. They're for us. They make a codebase feel tidy. They let us show off a clever pattern in a PR. They make a senior dev feel like the architecture is under control. I've done all of those things, and I still feel the pull.

But if the main reader and writer of the code is now an agent, and the main job of the human is reviewing what it did, then the question is what makes that loop faster and safer. In my experience the answer is almost always code that is flat, explicit and local. Code where the diff tells the whole story. Even if you see the same shape a few times.

Abstraction isn't bad. Premature abstraction is, and it has always been. The agentic age just made it a lot cheaper to wait.

How to Start Practicing RYAN

  1. Stop abstracting on the second occurrence. Wait for the third, or the fifth, or the first real bug caused by the duplication.
  2. Tell your agent the rule. Put it in your CLAUDE.md or AGENTS.md: prefer self-contained templates and services over shared helpers with flags, and don't extract shared code unless asked. Left alone, most models will happily DRY things up for you, because that's what their training data told them good code looks like.
  3. Unwind the worst offenders. Find the partial with the most parameters or the base class with the most overrides. Split it into the concrete things it's pretending to be. This is a great job for an agent.
  4. Keep knowledge DRY, ruthlessly. Everything above only works if the rules and tokens live in one place.

I fully expect some of you to disagree with this, and I'd honestly like to hear it. Maybe your team has a shared component library that's working beautifully with agents. Maybe you've been burned by copy-paste drift in a way that no test would catch. Those are real stories, and I'd rather hear them than pretend this is settled.

But next time you're about to extract a helper because two things look alike, ask yourself who you're really doing it for. If the honest answer is "future me, who won't be typing this anyway," maybe just repeat yourself. As needed.