To DRY or not to DRY, it is a matter of boundaries

Edit this page | 4 minute read

For a very VERY long time I've been convinced that aggressively refactoring your code in such a way that every piece of logic is repeated only once is a good thing. This practice, often referred to as DRY, or Don't Repeat Yourself, has been one of the many of my tools of choice and goes hand in hand with principles like S.O.L.I.D. Some developers just are incredibly 'skilled' in cutting and pasting code all over the place, so I've been in need of this a lot over the years. But with any 'best practice', you tend to go through a couple of phases before you realize that the world is not black and white. Just look at the adoption process of things like unit testing and mocking. So before you start to think I disapprove of DRY, don't worry, I won't. But it's important to realize that even DRY needs boundaries. It took me a few years…

So my first question is how far your efforts to apply DRY go. If your codebase comprises of several projects all available from the same Visual Studio solution (or your favorite IDE's equivalent), then chances are that that's how far you'll go. I doubt you will hunt down instances of the same code in other libraries and replace those with a single shared implementation. It probably won't even be possible. But what if that library's code was directly visible? What would you do then? The point is that the scope of your refactorings is kind of defined by the tool or source control repository you're working in.

My second question is about how you actually accomplish DRY? Do you introduce some shared project, component or folder that all your other code uses? And what appearance does that thing have? Is it more like a library that you use? Or does it feel like a framework that requires your code to implement some shared interface or inherit some common base-class? The difference is subtle but important. I'm not sure who said this, but a great phrase to illustrate the distinction is this:

"A library you use. A framework uses you."

I'm sure you have heard about design principles like Decrease Coupling, Increase Cohesion. The second part kind of that principle aligns well with DRY, but the first doesn't. Why? Well, if you decide to move everything related to a particular concept to one place (and thereby increasing cohesion) and you use that thing everywhere, haven't you really created an immense amount of coupling? And that's exactly the flipside of the coin. But that problem isn't so bad if the thing you're sharing is just some utility code that you use. You can easily duplicate the code when it's time to extract parts of your system into something new. But if that thing you're sharing relies on some well-known interface that many parts implement in order to connect them together, the problem gets worse. Especially if you're code base is large, this coupling gets sprinkled all over the place.

Now that I've explained the dark side of DRY and I've mentioned the scope of it, it should become evident that DRY still has a place in every developer's toolbox. As long as you constrain its application to a well-defined boundary, you'll benefit from it. But outside that boundary? Duplicate the code or wrap it in a really focused library which lifecycle is defined by another source control repository. If you're in the .NET space, you can solve this even nicer by using source-only NuGet packages and distribute them through MyGet. I realize that it may feel unnatural to duplicate code within the same source code repository. But imagine that those boundaries were used to split up the code in different solutions or repositories. I doubt you would try to apply DRY there. Next to that, in my experience, trying to use the same code in different places tends to put you on the path of either overly generic or leaky abstractions. Better to have two, more specific implementations instead. Frameworks are notorious for this, so if you really need a framework-ish solution, be very very careful to decouple the code that needs it from the framework (and .NET delegates solve this pretty nicely).

Identifying the right boundaries is the difficult part, although I'm fairly confident in saying that your IDE's solution structure is never the right boundary. If you're practicing Domain Driven Design and you have identified the Bounded Contexts, you have a great starting point. You still might want to find a smaller boundary within that such as for instance groups of classes that always change together, but never go beyond the Bounded Context. Architectural layers should also be seen as boundaries, even if the code is grouped in the same IDE project. A more functional view can be very beneficial as well, especially if you can identify vertical slices of functionality. Greg Young recently shared a more inside-out approach by stating that you should design your code in such a way that you can rewrite any part of it in a single day. It’s a rather extreme approach, but it will help against overzealous use of DRY.

So what do you think? Do you still believe in DRY or did you abandon it completely? And if still practice it, how do you define the boundaries and seams in your system? I'd love to hear your thoughts by commenting below. Oh, and follow me at @ddoomen to get regular updates on my everlasting quest for better solutions.

Leave a Comment