In software, the “DRY” principle stands for “Don’t Repeat Yourself” and is an adage about deduplicating code. (The contrasting acronym is WET for Write Everything Twice.)
DRY is a helpful reminder to avoid the Copy+Paste+Tweak coding anti-pattern which leads to very similar, slightly different code throughout our application. It reminds us to create abstractions for frequently used ideas.
However, we have found over the past few decades of software development that centralization (in organization structure and code) creates bottlenecks. DRY centralizes, and centralization does not always suit.
Domain vs Helper
Domain code is the stuff that matters to our customers and product owners. It’s the stuff that defines the business rules; it’s the value-add of our application. The important stuff.
In addition to domain concepts like business rules, we have a lot of “plumbing” code. We sometimes call this helper code. It’s code that transforms between data formats, handles errors, translates text, etc. It’s a necessary evil – we can’t work without it, but if there was none of it, that would be fine.
DRY your Domain liberally
In Domain Driven Design, we endeavor to have a canonical place in the codebase where each important domain concept is described.
We DRY our domain code by doing things like:
- extracting common logic to a single place
- creating micro-objects instead of primitive types for significant values
- splitting complex classes into a few well-named parts
- etc.
We want our domain code DRY so that when we need to support new behavior, there is a canonical place to change the behavior.
If two domain concepts are unrelated but happen to have some similar math or similar data, we should not DRY them up and merge them together. In the real world they are independent and vary independently, and our code should have the same characteristics. To DRY those incidentally duplicated parts of our code would be to make our code harder to change, because we’ll need to change one usage of the code without changing the other usage. Unhelpful DRYing leaves our code SCORCHED (see below).
DRY your helpers judiciously
In the course of writing our domain code, we have to shore it up with helper code. If we insist on DRYing the helper code, we are likely to couple unrelated domain concepts together.
Does that mean we must never share helper code? Surely not! For example, every time you import an off the shelf library for a generic task, that’s helper code. If you use it throughout your codebase, that’s shared helper code. And then, sometimes a library doesn’t exist, so you write your own. That’s shared helper code, and it’s normal and useful. The principle here is not “zero shared helper code”.
But what can happen in a project is that helper code is shared code by default.
It’s all public
. This leads to “SCORCHED” code (see below).
As a rule of thumb, you’ll do well to start all helper code as local, private, and bespoke. When you discover a need for some shared utility, then extract the local, private, bespoke helper as a shared utility. Think of this like publishing a library – except you’re only “publishing” it within your codebase. You consider it to be mature and valuable enough to be worth the coupling cost, so you make it a shared helper.
But if every helper is a shared helper (out of dedication to DRY), we can end up with code so DRY that it’s SCORCHED.
So DRY it’s SCORCHED
If you over-DRY your helper code, it will end up SCORCHED.
-
Spaghetti – helper code is depended upon by several unrelated parts of
the codebase
- Fix: sometimes two parts of code look similar but represent different
domain concepts. If the requirements will change independently, then let
the code also be independent. You can enforce this with static analysis
tools that let you express rules like “files from
./src/**/*
can only import from their own top level folders or from./src/**/public-api.ts
” - Principle: divide your code into independent modules that only interact via established, published interfaces.
- Fix: sometimes two parts of code look similar but represent different
domain concepts. If the requirements will change independently, then let
the code also be independent. You can enforce this with static analysis
tools that let you express rules like “files from
-
COnditionals everywhere – DRY code with flags and conditionals to
vary how it behaves
- Fix: instead of DRY code with complex conditionals, consider repeating the code without the conditionals: have one copy with one behaviour and the other copy with the other. Model it as two algorithms instead of one conditional algorithm.
- Principle: Use
if
only as a guard clause.
-
Randomly organized (as opposed to
domain-driven) – when we make code reuse our goal, we twist our codebase to
make it easy to share helper code.
- Fix: instead of designing for reuse, design for domain fit. Structure the code by vertical slice of customer value. Use the words customers and product owners use for your code. And then write whatever bespoke, duplicated helper code you need to support those independent, domain-aligned chunks of code.
- Principle: develop a shared mental model of the domain, and express that model in your code structure. Repeat whatever you need to to keep that structure clear.
-
CHErished – you can’t delete code because “someone else is using it”
- Fix: by keeping helpers local to where they are used – keeping them private – it is easier to see how to change (or delete) some code. When it belongs to everyone, we get a sense that someone cherishes it as it is and we’re less likely to change it. Maybe we add a conditional and put our change inside the if – further complicating the code.
- Principle: write code that is easy to delete
-
Diffuse (as opposed to single responsibility) –
the intent of the code is unclear because of diverse usages
- Fix: prefer smaller, narrowly-scoped helpers that do one thing. When we need a similar helper for a different thing, let’s make a different helper.
- Principle: Single Responsibility Principle
DRY your ideas, not your code
It can be helpful to frame DRY as a tool for clarifying our ideas, not a tool for compressing our code. Let there be a single place in our code where each important idea is expressed (keep the ideas DRY). Accept duplication in the helper code that supports those independent ideas – accept WET code if it keeps the important ideas clear and independent.
Don’t Insist on DRY
When we constrain ourselves to repeat nothing, we have to give up other more important goals. While DRY is one useful principle, it is not always helpful. If we insist that our code is always DRY, we’re apt to make it SCORCHED.
- Spaghetti
- COnditionals everywhere
- Randomly organized
- CHErished (hard to delete)
- Diffuse (multiple responsibilities)