Stephan Schmidt

Sometimes DRY is not the right thing

Beware of Don't Repeat Yourself as a mantra


If one battle cry unites all developers, from Java to TypeScript, from Haskell to Elm, it’s “Don’t repeat yourself! (DRY)”. Everyone is aligned on that one.

“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system” The Pragmatic Programmer

Blindly following this rule leads to disaster.

Lets go into some detail using abstractions as an example for DRY. Take a very simplified example from Inkmi - Dream Jobs for CTOs. In it’s core, Inkmi is a marketplace with employers and #CTOs. CTOs define their dream jobs and a company describes their job offer. CTOs define their salary, and if they want remote work and a four hour work week, the company describes their company policy. When there is a match, the CTO got their dream job (simplified ;-)

How does this work on a code level?

You need to define the company policy twice. On one side, you define the company that you seek, and the company that offers jobs on the other side.

type JobOffer struct{
  RemoteFirst bool
}

type DreamJob struct {
  RemoteFirst bool
}

Then you can match a JobOffer to a DreamJob.

But now we have the same company policy in two types. What if we want to rename it? Replace the bool with an int for “Remote First”, “Hybrid”, “No Remote”? You need to change two places. You need to remember all the places! Don’t repeat yourself!

You might abstract the code into a Company struct that is used by both sides of the marketplace.

// This struct we want to reuse.
type Company struct {
  RemoteFirst bool
}

type JobOffer  struct{
  Company Company
}

type DreamJob struct {
  Company Company
}

This way, we can add to Company without needing to change JobOffer and DreamJob. Code that depends on Company - like CheckCompanyPolicy - can now be reused.

You want to add the size of the company in people, because a dream job might depend on the size of the company (a CTO only wants to work for small or large companies):

type Company struct {
  RemoteFirst bool
  People int
}

But there might be a range where the CTO is comfortable with, so you write

type Company struct {
  RemoteFirst bool
  MinPeople int
  MaxPeople int
}

Voila!

As you have abstracted the part of the concept dream-job and job-offer into Company, the company offering a job now also has MinPeople and MaxPeople. Which doesn’t make sense. So for companies you decide that MinPeople == MaxPeople. Problem solved. But to keep your abstraction working, you added technical debt. You might try to solve it with

type Company struct {
  RemoteFirst bool
  People people
}

// Not Go code, you'd use an interface
type CompanyPeople extends People {
  Count int
}

type JobOfferPeople extends People {
  Min int
  Max int
}

Which creates a hierarchy of entanglement. JobOffer in package job, something specific, depends on Company in package general, something abstract, which depends on CompanyPeople in package company, something specific again. Which makes understanding your type hierarchy difficult and entangled with many parts of your application.

Just because you wanted to save something that you abstracted, that wasn’t the same thing in the beginning.

Words fool us, the company in JobOffer is something different as a concept than company in DreamJob. You have been fooled by the name and the moment in time, where coincidently, these two concepts were close enough that you could abstract them. Then time flowed on, and the concepts diverged again, pulling Company in two different directions.

On top of this if we later want to split development into two teams, DreamTeam and OfferTeam, they have a dependency on the same code in Company—who owns Company? Co-owned? Who has the final say in changes, what if it is changed in incompatible ways? What if it is changed by two teams at the same time? What if we want to create two microservices? Abstractions create entanglement and dependencies.

Programming is breaking down a problem into smaller problems and those into even smaller problems, until you can write a solution as code—perhaps a method or a class (JobOffer and DreamJob from our example). After you’ve done this several times, you find the common thing between all the problems and create an abstraction, a class to be reused, an interface, a framework.

Given a set of code, there are many possible abstractions. Why do we DRY with abstract code in the first place? One reason is to make changes easier. You only need to change one part, and all those use cases the abstraction covers, are changed.

Abstractions assume things, most often the future. No one can predict the future, or we would all play the lottery and the lottery would go broke because everybody only wins. I don’t play the lottery, I can’t predict the future.

When you make abstractions, you need to be right about the future (future changes). There is one way to look into the future: Look at the vision and strategy of your company. If the company has a vision for the next 5 to 10 years, and a strategy to reach that vision, you kind of can look into the future of your code (there might be changes to the vision, or strategy though, argh).

This brings us back to DRY. There are things for DRY, like count orders

var count int
for _, o := range orders {
  if order.amount > 0 {
    count++
  }
}

You would have that same code in several places but deduplicate to a function

func countNonZeroOrders(orders []Order) int {
  var count int
  for _, o := range orders {
    if order.amount > 0 {
      count++
    }
  }
  return count
}

Then, as showed above, there are places where DRY leads to disaster. DRY is a tricky thing. You need to do it, to keep your code base manageable. But making the wrong things DRY, you create coupling and make your code more unmanageable.

Takeaway: If things do change at the same time, in the same direction and are the same thing, DRY. Replacing duplicate code then makes sense. But just because code is the same in two places doesn’t mean it is the same code that can be deduplicated or abstracted. Don’t overdue DRY.

More in my Upcoming Book

Amazing CTO Book Cover

Technical Debt

The essential guide

Everyone has technical debt. Eveyone wants to get out of technical debt.

Join CTO Newsletter

Join more than 2700 CTOs and Engineering Managers

More Stuff from Stephan

Other interesting articles for CTOs

Best books for CTOThe CTO BookExperienced CTO CoachEngineering Manager CoachingConsulting and Workshops to Save you TimeCTO MentorCTO MentoringCTO NewsletterHow many developers do you need?Postgres for Everything Product Roadmaps for CTOsHow to become a CTO in a company - a career path

Other Articles

Best Books for a CTO

Scrum is no longer fit with remote work

Comparing CPUs for Development

Selfhealing Code for Startup CTOs and Solo Founders

Trust is Not a One-Way Street