Updated on 06 Jul, 202641 mins read 107 views

Imagine you are building an e-commerce system.

You need to calculate tax.

Order Module:

double calculateTax(double amount)
{
    return amount * 0.18;
}

Invoice Module:

double calculateTax(double amount)
{
    return amount * 0.18;
}

Payment Module:

double calculateTax(double amount)
{
    return amount * 0.18;
}

Reporting Module:

double calculateTax(double amount)
{
    return amount * 0.18;
}

Everything works.

Until one day:

Tax Rate Changed
18% to 20%

Now developers must find:

Every Tax Calculation

in the entire codebase.

Some missed one location.

Result:

Orders = 20%
Invoices = 20%
Reports = 18%

Business inconsistency.

Production bugs.

Customer complaints.

Financial discrepancies.

This exact problem led to one of the most influential principles in software engineering.

DRY – Don't Repeat Yourself.

Historical Background

DRY was introduces in the book: The Pragmatic Programmer

by:

  • Andrew Hunt
  • David Thomas

Their original statement:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

Notice something important.

They did NOT say:

Don't Repeat Code

Most developers think DRY simply means:

“Don't copy and paste code.”

They said:

Don't Repeate Knowledge

This distinction is critical.

The Most Common Misunderstanding

Most developers think:

DRY = No Duplicate Code

Wrong.

Actual DRY:

DRY = No Duplicate Knowledge

These are not the same thing.

Understanding Knowledge Duplication

Suppose:

double calculateGST(double amount)
{
    return amount * 0.18;
}

And elsewhere:

double calculateInvoiceGST(double amount)
{
    return amount * 0.18;
}

This duplicates:

Tax Rule Knowledge

If tax changes, both must change.

This violates DRY.

Understanding Code Duplication

Now consider:

void printHeader()
{
    std::cout << "Welcome";
}

And:

void printFooter()
{
    std::cout << "Thank You";
}

Some developers attempt:

void printMessage(...)

to avoid duplication.

But:

Header and Footer
Represent Different Knowledge

Even if code looks similar.

DRY is NOT violated.

The Single Source of Truth

The core goal of DRY is:

One Fact
One Location

Example:

Bad:

const double GST_ORDER = 0.18;
const double GST_INVOICE = 0.18;
const double GST_PAYMENT = 0.18;

Good:

class TaxConfiguration
{
public:

    static constexpr double GST = 0.18;
};

Now:

One Truth

exists.

Changes happen once.

Consistency is preserved.

Why Duplication Is Dangerous

Every duplicated piece of knowledge creates:

Multiple Maintenance Points

Example:

Tax Rule

appears:

12 Times

New regulation arrives.

Developer updates:

11 Places

Misses:

1 Place

Bug appears.

This is called:

Shotgun Surgery

One change requires modifying many locations.

A classic code smell.

DRY Applies to

  • Business rules: If “users must be 18 or older” is a rule, it should be defined once, not checked in six different places with slighlty different age thresholds.
  • Configurations: Database connection strings, API keys, and timeout values should live in one config file, not scattered across multiple classes.
  • Data models: If a User has a name and email, that structure should be defined once, not redefined in every module that touches user data.
  • Documentation: If your API docs decribe a field as “ISO 8601 date format” that definition should come from one source, not manually written in three different doc pages.
  • Tests: Shared setup logic (like creating test users or populating a database) should be extracted into helpers rather than copy-pasted across test files.

Whenever the same concept appears in more than one place, you introduce redundancy. Redundancy makes your system harder to maintain and more prone to bugs.

Rule of Three

Before you start extracting every bit of repeated code into a shared utility, there is a important guideline to keep in mind: the Rule of Three.

A common guideline:

Do NOT abstract after: 1 occurrence

Be cautious after: 2 occurrences

Strongly consider abstraction after: 3 corrences

Known as: Rule of Three

The idea is simple. Before extracting shared logic, wait until you see the same pattern three times. Two occurrences might be coincidental. Maybe those two pieces of code look similar today but will diverge tomorrow as their respective features evolve. Three occurrences, though, that is a pattern.

At that point, you have strong evidence that the duplication represents genuine shared knowledge, and extracting it into a single location is the right call.

Real-World Example

Imagine you are building a system to manage users across three modules: authentication, payments, and messaging. Each module contains its own copy of the email validation logic.

// In AuthService.cpp
bool isValidEmail(const std::string& email) {
    return !email.empty() && email.find('@') != std::string::npos
        && email.find('.') != std::string::npos;
}

// In PaymentService.cpp
bool isValidEmail(const std::string& email) {
    return !email.empty() && email.find('@') != std::string::npos
        && email.find('.') != std::string::npos;
}

// In MessagingService.cpp
bool isValidEmail(const std::string& email) {
    return !email.empty() && email.find('@') != std::string::npos
        && email.find('.') != std::string::npos;
}

Now suppose the business changes the rule: email addresses must now end with .com or .org

If this logic is duplicated across three modules, you need to update every single location. Miss even one, and the system becomes inconsistent. Users might pass validation in the auth module but fail in the payments module, or vice versa. You have created technical debt that will only grow worse over time.

Applying DRY

Let's refactor the email validation example by extracting the common logic into a single, shared location.

Step 1: Create a Utility Class

Extract the validation logic into a dedicated class that becomes the single source of truth.

class EmailValidator {
public:
    static bool isValid(const string& email) {
        if (email.empty()) return false;
        bool hasAt = email.find('@') != string::npos;
        bool hasDot = email.find('.') != string::npos;
        bool validEnding = email.size() >= 4
            && (email.substr(email.size() - 4) == ".com"
                || email.substr(email.size() - 4) == ".org");
        return hasAt && hasDot && validEnding;
    }
};

Step 2: Use It Across Modules

Now every modules delegates to the shared validator instead of implementing its own.

// In AuthService.cpp
if (EmailValidator::isValid(user.getEmail())) {
    // Proceed with authentication
}

// In PaymentService.cpp
if (EmailValidator::isValid(customer.getEmail())) {
    // Proceed with payment processing
}

// In MessagingService.cpp
if (EmailValidator::isValid(recipient.getEmail())) {
    // Proceed with sending message
}

Now the email validation logic lives in one place. Any future updates, like adding regex-based validation or supporting new top-level domains, only need to be made once. All three modules stay consistent automatically.

When it is Okay to Repeat

This principle is not a strict rule. There are situations where a bit of repetition produces better code than a forced abstraction.

1 Avoid Premature Abstraction

Do not extract shared code too early. Let duplication reveal itself first. Abstractions created too soon can be misleading or hard to maintain.

"Duplication is far cheaper than the wrong abstraction."

2 Keep It Simple

If a line of code is extremely simple and unlikely to change, extracting it into a shared utility can actually make things worse. Creating a MathUtils.addOne(x) method to avoid writing x + 1 in two places is not DRY. It is overengineering. The overhead of finding, understanding, and navigating to the shared method outweighs the benefit of eliminating the trivial duplication.

Practical Example

Let's now dirty our hands in the practical example.

The Problem:

We have three services CustomerService, SellerService, and DeliveryPartnerService. Each service currently duplicates two pieces of logic: validation and logging.

Before: Violating DRY

#include <iostream>
#include <string>
using namespace std;

class CustomerService {
public:
    void registerCustomer(const string& name) {
        // Duplicated validation
        if (name.empty()) {
            cout << "Name cannot be empty.\n";
            return;
        }

        // Duplicated logging
        cout << "[LOG] Registering customer...\n";

        cout << "Customer " << name << " registered successfully.\n";

        // Duplicated logging
        cout << "[LOG] Registration completed.\n";
    }
};

class SellerService {
public:
    void registerSeller(const string& name) {
        // Duplicated validation
        if (name.empty()) {
            cout << "Name cannot be empty.\n";
            return;
        }

        // Duplicated logging
        cout << "[LOG] Registering seller...\n";

        cout << "Seller " << name << " registered successfully.\n";

        // Duplicated logging
        cout << "[LOG] Registration completed.\n";
    }
};

class DeliveryPartnerService {
public:
    void registerPartner(const string& name) {
        // Duplicated validation
        if (name.empty()) {
            cout << "Name cannot be empty.\n";
            return;
        }

        // Duplicated logging
        cout << "[LOG] Registering delivery partner...\n";

        cout << "Delivery Partner " << name << " registered successfully.\n";

        // Duplicated logging
        cout << "[LOG] Registration completed.\n";
    }
};

Why does this violate DRY?

The following code is repeated in all three classes:

if (name.empty()) {
    cout << "Name cannot be empty.\n";
    return;
}

and

cout << "[LOG] Registration completed.\n";

If the validation rule changes (for example, the name must be at least 3 characters long), you will have to update it in three different places. This duplication increases maintenance cost and the chance of inconsistencies.

DRY version

Extract the repeated logic into a helper class.

#include <iostream>
#include <string>
using namespace std;

class RegistrationHelper {
public:
    static bool validateName(const string& name) {
        if (name.empty()) {
            cout << "Name cannot be empty.\n";
            return false;
        }
        return true;
    }

    static void logStart(const string& role) {
        cout << "[LOG] Registering " << role << "...\n";
    }

    static void logEnd() {
        cout << "[LOG] Registration completed.\n";
    }
};

class CustomerService {
public:
    void registerCustomer(const string& name) {
        if (!RegistrationHelper::validateName(name))
            return;

        RegistrationHelper::logStart("customer");

        cout << "Customer " << name << " registered successfully.\n";

        RegistrationHelper::logEnd();
    }
};

class SellerService {
public:
    void registerSeller(const string& name) {
        if (!RegistrationHelper::validateName(name))
            return;

        RegistrationHelper::logStart("seller");

        cout << "Seller " << name << " registered successfully.\n";

        RegistrationHelper::logEnd();
    }
};

class DeliveryPartnerService {
public:
    void registerPartner(const string& name) {
        if (!RegistrationHelper::validateName(name))
            return;

        RegistrationHelper::logStart("delivery partner");

        cout << "Delivery Partner " << name << " registered successfully.\n";

        RegistrationHelper::logEnd();
    }
};

This version follows the DRY principle because:

  • Validation logic exists in one place.
  • Logging logic exists in one place.
  • If the validation or logging behavior changes, you only modify RegistrationHelper, and all services automatically use the updated behavior.
Buy Me A Coffee

Leave a comment

Your email address will not be published. Required fields are marked *