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 Calculationin 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 CodeWrong.
Actual DRY:
DRY = No Duplicate KnowledgeThese 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 KnowledgeIf 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 KnowledgeEven if code looks similar.
DRY is NOT violated.
The Single Source of Truth
The core goal of DRY is:
One Fact
One LocationExample:
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 Truthexists.
Changes happen once.
Consistency is preserved.
Why Duplication Is Dangerous
Every duplicated piece of knowledge creates:
Multiple Maintenance PointsExample:
Tax Ruleappears:
12 TimesNew regulation arrives.
Developer updates:
11 PlacesMisses:
1 PlaceBug appears.
This is called:
Shotgun SurgeryOne 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.

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