Skip to main content

DRY Principle

What is DRY?

DRY, which stands for "Don't Repeat Yourself," is a fundamental principle of software development aimed at reducing repetition in code. The principle was formulated by Andy Hunt and Dave Thomas in their book "The Pragmatic Programmer" and is stated as:

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

The essence of DRY is to avoid duplicating code, logic, or data across a system. When the same information is expressed in multiple places, changes require updates in multiple locations, increasing the chance of inconsistencies and errors.

The DRY Principle

It's like having a single source of truth. Instead of writing the same instructions in multiple places, you write it once and refer to it whenever needed. This ensures consistency and makes changes easier.

Why DRY Matters

Reduced Maintenance Burden

When logic exists in only one place, updates require changes to only that single location. This drastically reduces the maintenance effort and the risk of overlooking update locations.

Improved Consistency

With a single source of truth, you eliminate the risk of inconsistent implementations or contradictory logic across the codebase.

Enhanced Code Quality

DRY code tends to be more organized and modular. The process of eliminating duplication often leads to better abstractions and cleaner architecture.

Better Scalability

As your application grows, DRY principles help manage complexity by keeping the codebase size proportional to its functionality rather than its repetition.

Easier Testing

With functionality consolidated in one place, you can focus testing efforts on that single implementation rather than testing duplicate code.

DRY in Practice

Example 1: Constants Management

Consider a web application with error messages repeated throughout the code:

Centralizing Error Messages

Moving repeated strings to a central constants file improves maintainability and consistency

Repetitive ApproachAvoid
// Without DRY - error messages repeated in multiple places
function validateEmail(email) {
if (!email.includes('@')) {
return "Please enter a valid email address.";
}
return null;
}

function registerUser(user) {
if (!user.email.includes('@')) {
throw new Error("Please enter a valid email address.");
}
// Registration logic
}

function updateUserEmail(userId, newEmail) {
if (!newEmail.includes('@')) {
return {
success: false,
error: "Please enter a valid email address."
};
}
// Update logic
}
DRY ApproachRecommended
// With DRY - centralized error messages
const ErrorMessages = {
INVALID_EMAIL: "Please enter a valid email address.",
USERNAME_TAKEN: "This username is already taken.",
PASSWORD_TOO_SHORT: "Password must be at least 8 characters long.",
UNAUTHORIZED: "You do not have permission to access this resource."
};

function validateEmail(email) {
if (!email.includes('@')) {
return ErrorMessages.INVALID_EMAIL;
}
return null;
}

function registerUser(user) {
if (!user.email.includes('@')) {
throw new Error(ErrorMessages.INVALID_EMAIL);
}
// Registration logic
}

function updateUserEmail(userId, newEmail) {
if (!newEmail.includes('@')) {
return {
success: false,
error: ErrorMessages.INVALID_EMAIL
};
}
// Update logic
}

This approach:

  1. Centralizes all error messages in one place
  2. Makes updates to message text consistent across the application
  3. Allows for easier localization/internationalization
  4. Improves code readability by using meaningful constants

Example 2: Shared Validation Logic

Consider validation logic that's used across multiple components:

Extracting Common Validation Logic

Creating reusable validation functions eliminates duplication across forms

Duplicated ValidationAvoid
// Without DRY - repeated validation logic
function UserForm() {
function validateForm(formData) {
if (!formData.name.trim()) {
return "Name is required";
}

if (!formData.email.includes('@')) {
return "Invalid email format";
}

if (formData.password.length < 8) {
return "Password must be at least 8 characters";
}

return null;
}

// Form handling logic
}

function AdminUserForm() {
function validateForm(formData) {
if (!formData.name.trim()) {
return "Name is required";
}

if (!formData.email.includes('@')) {
return "Invalid email format";
}

if (formData.password.length < 8) {
return "Password must be at least 8 characters";
}

if (!formData.role) {
return "Role is required";
}

return null;
}

// Form handling logic
}
Shared ValidationRecommended
// With DRY - centralized validation logic
const UserValidation = {
validateName(name) {
return name.trim() ? null : "Name is required";
},

validateEmail(email) {
return email.includes('@') ? null : "Invalid email format";
},

validatePassword(password) {
return password.length >= 8 ? null : "Password must be at least 8 characters";
}
};

function UserForm() {
function validateForm(formData) {
return UserValidation.validateName(formData.name) ||
UserValidation.validateEmail(formData.email) ||
UserValidation.validatePassword(formData.password) ||
null;
}

// Form handling logic
}

function AdminUserForm() {
function validateForm(formData) {
return UserValidation.validateName(formData.name) ||
UserValidation.validateEmail(formData.email) ||
UserValidation.validatePassword(formData.password) ||
(formData.role ? null : "Role is required");
}

// Form handling logic
}

This approach:

  1. Centralizes validation rules in one location
  2. Makes validation consistent across different forms
  3. Allows for easier updates to validation logic
  4. Supports composition of validation rules for different contexts

Example 3: Helper Functions

Consider formatting functions used across an application:

Creating Shared Utility Functions

Moving common formatting logic to a utilities file promotes reuse and consistency

Repeated FunctionsAvoid
// Without DRY - repeated formatting logic
function UserProfile({ user }) {
function formatDate(date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}

return (
<div>
<h1>{user.name}</h1>
<p>Member since: {formatDate(user.joinDate)}</p>
</div>
);
}

function ActivityLog({ activities }) {
function formatDate(date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}

return (
<div>
<h2>Recent Activity</h2>
<ul>
{activities.map(activity => (
<li key={activity.id}>
{activity.description} - {formatDate(activity.date)}
</li>
))}
</ul>
</div>
);
}
Shared UtilitiesRecommended
// With DRY - centralized formatting functions
// utils/formatters.js
export function formatDate(date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}

export function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
}

export function formatPhoneNumber(phone) {
// Format phone number logic
}

// Components using the shared utility
import { formatDate } from '../utils/formatters';

function UserProfile({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>Member since: {formatDate(user.joinDate)}</p>
</div>
);
}

function ActivityLog({ activities }) {
return (
<div>
<h2>Recent Activity</h2>
<ul>
{activities.map(activity => (
<li key={activity.id}>
{activity.description} - {formatDate(activity.date)}
</li>
))}
</ul>
</div>
);
}

This approach:

  1. Centralizes formatting logic in a dedicated utilities file
  2. Ensures consistent formatting across the application
  3. Makes it easy to update formatting logic in one place
  4. Improves code organization and reusability

Applying DRY in Your Code

Do

  • Extract common logic: Move repeated code into functions or classes
  • Use constants: Define constants for values used in multiple places
  • Create helper modules: Organize shared functionality in dedicated modules
  • Use inheritance appropriately: Share functionality through class inheritance
  • Apply composition: Use object composition to reuse behavior

Don't

  • Copy-paste code: Avoid duplicating code between different parts of the application
  • Define the same logic multiple times: Consolidate logic into a single location
  • Hardcode values: Replace hardcoded values with named constants
  • Create redundant documentation: Ensure documentation is synchronized with code
  • Implement parallel hierarchies: Avoid creating multiple class hierarchies that mirror each other

Finding and Fixing DRY Violations

Identifying DRY Violations

Signs that you might be violating DRY include:

  1. Copy-pasted code: Blocks of code that are identical or nearly identical
  2. Similar functions: Functions that perform almost the same task with minor variations
  3. Repeated string literals: The same text appearing in multiple places
  4. Duplicated validation rules: The same validation logic implemented in multiple places
  5. Parallel updates: Having to make the same change in multiple places

Techniques for Eliminating Duplication

1. Extract Method

Move duplicated code into a separate method that can be called from multiple places.

// Before extraction
function processUser(user) {
// 20 lines of validation logic

// Save user
database.save(user);
}

function processAdmin(admin) {
// Same 20 lines of validation logic

// Save admin with special handling
admin.role = 'admin';
database.save(admin);
}

// After extraction
function validatePerson(person) {
// 20 lines of validation logic
}

function processUser(user) {
validatePerson(user);
database.save(user);
}

function processAdmin(admin) {
validatePerson(admin);
admin.role = 'admin';
database.save(admin);
}

2. Template Method Pattern

Define a skeleton of an algorithm in a method, deferring some steps to subclasses.

// Using the Template Method pattern
class DocumentProcessor {
process(document) {
this.validate(document);
this.preprocess(document);
this.save(document);
this.postprocess(document);
}

validate(document) {
// Common validation logic
}

preprocess(document) {
// To be overridden by subclasses
}

save(document) {
// Common saving logic
}

postprocess(document) {
// To be overridden by subclasses
}
}

class InvoiceProcessor extends DocumentProcessor {
preprocess(document) {
// Invoice-specific preprocessing
}

postprocess(document) {
// Invoice-specific postprocessing
}
}

class ContractProcessor extends DocumentProcessor {
preprocess(document) {
// Contract-specific preprocessing
}

postprocess(document) {
// Contract-specific postprocessing
}
}

3. Strategy Pattern

Define a family of algorithms, encapsulate each one, and make them interchangeable.

// Using the Strategy pattern
class PaymentProcessor {
constructor(paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}

processPayment(amount) {
return this.paymentStrategy.process(amount);
}
}

class CreditCardPayment {
process(amount) {
console.log(`Processing credit card payment of ${amount}`);
// Credit card processing logic
}
}

class PayPalPayment {
process(amount) {
console.log(`Processing PayPal payment of ${amount}`);
// PayPal processing logic
}
}

class BitcoinPayment {
process(amount) {
console.log(`Processing Bitcoin payment of ${amount}`);
// Bitcoin processing logic
}
}

// Usage
const processor = new PaymentProcessor(new CreditCardPayment());
processor.processPayment(100);

// Change strategy at runtime
processor.paymentStrategy = new PayPalPayment();
processor.processPayment(100);

4. Constants and Enumerations

Use constants or enums for values that appear multiple times.

// Using constants
const HttpStatus = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
SERVER_ERROR: 500
};

function handleResponse(response) {
switch (response.status) {
case HttpStatus.OK:
return processSuccess(response);
case HttpStatus.UNAUTHORIZED:
return redirectToLogin();
case HttpStatus.FORBIDDEN:
return showPermissionError();
case HttpStatus.NOT_FOUND:
return showNotFoundError();
default:
return showGenericError();
}
}

5. Configuration Files

Move repeated configuration values to external configuration files.

// config.js
export default {
apiUrl: 'https://api.example.com',
maxRetries: 3,
timeout: 5000,
pageSize: 20,
dateFormat: 'YYYY-MM-DD',
supportEmail: 'support@example.com'
};

// Usage in components
import config from './config';

function fetchData() {
return fetch(`${config.apiUrl}/data`, {
timeout: config.timeout
});
}

function showError() {
return `Contact ${config.supportEmail} for assistance`;
}

Balancing DRY with Pragmatism

While DRY is a powerful principle, it should be applied pragmatically:

When to Apply DRY

  • When the same logic appears in multiple places
  • When changes would require updates in multiple locations
  • When abstractions improve code clarity and maintainability

When DRY Might Not Apply

  • When creating an abstraction would be more complex than the duplication
  • When seemingly similar code might evolve differently
  • When the cost of the abstraction outweighs the benefit
WET Principle

Sometimes it's better to repeat code once before creating an abstraction. The "Write Everything Twice" (WET) approach suggests waiting until you have at least two instances of duplication before refactoring, ensuring your abstraction solves a real problem.

Advanced DRY Techniques

Higher-Order Functions

Use higher-order functions to abstract common patterns:

// Without higher-order functions
function validateEmail(email) {
if (!email) return 'Email is required';
return null;
}

function validatePassword(password) {
if (!password) return 'Password is required';
return null;
}

function validateUsername(username) {
if (!username) return 'Username is required';
return null;
}

// With higher-order functions
function required(fieldName) {
return function(value) {
if (!value) return `${fieldName} is required`;
return null;
};
}

const validateEmail = required('Email');
const validatePassword = required('Password');
const validateUsername = required('Username');

Middleware Patterns

Use middleware patterns to create reusable processing chains:

// Middleware pattern for request processing
function createRequestProcessor(middlewares) {
return async function processRequest(request) {
let index = 0;

async function next() {
if (index >= middlewares.length) {
return;
}

const middleware = middlewares[index++];
await middleware(request, next);
}

await next();
return request;
};
}

// Middleware functions
async function authenticate(request, next) {
console.log('Authenticating request');
// Authentication logic
await next();
}

async function authorize(request, next) {
console.log('Authorizing request');
// Authorization logic
await next();
}

async function validate(request, next) {
console.log('Validating request');
// Validation logic
await next();
}

async function processData(request, next) {
console.log('Processing data');
// Data processing logic
await next();
}

// Create processor with middleware chain
const processRequest = createRequestProcessor([
authenticate,
authorize,
validate,
processData
]);

// Usage
processRequest({ url: '/api/data', body: { /* data */ } });

Code Generation

For extreme cases of repetition, consider code generation:

// Dynamic function creation for API endpoints
function createApiEndpoints(entityNames) {
const api = {};

for (const entity of entityNames) {
const capitalizedEntity = entity.charAt(0).toUpperCase() + entity.slice(1);

api[`get${capitalizedEntity}`] = async (id) => {
return fetch(`/api/${entity}/${id}`).then(res => res.json());
};

api[`create${capitalizedEntity}`] = async (data) => {
return fetch(`/api/${entity}`, {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
}).then(res => res.json());
};

api[`update${capitalizedEntity}`] = async (id, data) => {
return fetch(`/api/${entity}/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
}).then(res => res.json());
};

api[`delete${capitalizedEntity}`] = async (id) => {
return fetch(`/api/${entity}/${id}`, {
method: 'DELETE'
}).then(res => res.json());
};
}

return api;
}

// Usage
const api = createApiEndpoints(['user', 'product', 'order']);
api.getUser(1);
api.createProduct({ name: 'New Product', price: 29.99 });

Conclusion

DRY is a foundational principle that helps create maintainable, consistent codebases. By eliminating duplication, developers can make code more robust, easier to update, and less prone to inconsistencies.

Remember that DRY is not about eliminating all repetition, but about ensuring that each piece of knowledge has a single, authoritative representation. Applied with pragmatism, DRY leads to more maintainable, flexible, and elegant code.

When you find yourself copying and pasting code, stop and ask: "Should this be abstracted into a reusable component?" The answer will guide you toward cleaner, more maintainable code.