Mathieu Eveillard

Don’t Repeat Yourself… but just a little bit anyway

Don’t Repeat Yourself… but just a little bit anyway

DRY (Don’t Repeat Yourself) is an excellent principle. However, it shouldn’t be applied blindly, or it risks becoming counterproductive. Consider this example of price calculation.

When Genericity Makes No Business Sense

You’ll agree with me that something doesn’t quite add up when reading this (pseudo-)code. All these if statements catch our attention; the function’s execution flow is choppy. It’s clear that there are essentially two parallel paths (B2B and B2C), each of which is fairly simple.

That’s exactly right. Here, we abstracted too early. We abstracted logic that shouldn’t have been abstracted from a business perspective. The resulting code is a Frankenstein’s monster of genericity.

We were tempted to do it this way because there’s a similarity in form. Yes, we calculate a gross total, apply discounts, then VAT. But at each step the process is radically different, because the approach is B2B on the one hand and B2C on the other. In other words, the similarity in form does not correspond to any business reality (accidental vs. essential similarity).

What should set off alarm bells is that the function has two reasons for changing: some related to B2B, others related to B2C. By trying too hard to adhere to the sacrosanct DRY principle, we violated the SRP (Single Responsibility Principle) and introduced unwanted coupling. When you share code, you create a dependency. Every change to one use case risks breaking the other.

What would this code even look like if we had to work in a B2B2C scenario now? It would be a nightmare. The cost of poor abstraction is exponential: every new use case has to contend with all the previous ones.

So what should we do, and when should we share code?

Duplicate to Better Abstract

To answer that question, you still need to assess the similarity between the two algorithms. And so, as paradoxical as it may seem to those of us who were raised on DRY, the right approach is actually to duplicate the behaviors—at least initially.

Duplicate to see more clearly:

In this case, it’s better to duplicate the higher-level functions and rely on a few shared subfunctions.

In our example, this could result in the following pseudocode:

const computeB2CPrice = (context, items) => {
  // compute subtotal (unit price × quantity)
  // apply promo code if any (percentage or fixed amount)
  // apply loyalty points as discount (1 point = 0.01€)
  // apply seasonal sale if active (percentage on eligible items only)
  // compute VAT (single domestic rate, e.g. 20%)
  // round to 2 decimals
};

const computeB2BPrice = (context, items) => {
  // compute subtotal (negotiated unit price per client × quantity)
  // apply volume discount by tier (e.g. >1000 units: -5%, >5000: -12%)
  // apply contractual annual rebate if revenue target reached
  // determine VAT scheme:
  //   - domestic: standard VAT
  //   - intra-EU with valid VAT number: reverse charge (0%)
  //   - export outside EU: exempt
  // apply early payment discount if payment terms < 30 days (e.g. -2%)
  // round to 2 decimals
};

// Shared building blocks
const computeSubtotal = (items) => {
  /* ... */
};

const applyTaxRate = (amount, rate) => {
  /* ... */
};

const roundToDecimals = (amount, decimals) => {
  /* ... */
};

The execution flow of each function is now linear, making it easy to read. Each function can be tested using clear business scenarios. Each function is much easier to maintain and can evolve independently of the other.

Here, we might ask ourselves whether there aren’t, at the core, two distinct contexts (in the sense of Domain-Driven Design). I don’t think so: same purpose (calculating a price), therefore same context (pricing). The vocabulary is shared, as is much of the data; the only difference lies in the business rules—in other words, different strategies or policies within the same context.

Three strikes and you refactor

So let’s remember that a little duplication costs far less than poor abstraction, which you’ll be dragging around like a ball and chain for months. Especially when you’re getting to know the business, it’s crucial to wait before consolidating.

So let’s keep Martin Fowler’s recommendation in mind:

The first time you do something, you just do it. The second time you do something similar, you cringe at the duplication, but you go ahead and duplicate it anyway. The third time you do something similar, you refactor.

Obviously, this advice doesn’t apply to purely generic/technical functions (parsing, formatting…), which don’t require business logic to exist.

← All posts