This post introduces the DRY (Don’t Repeat Yourself) principle, a cornerstone of software maintainability. If you want to understand how DRY complements other principles, check out our posts on KISS and YAGNI first!
What Is the DRY Principle?
DRY stands for “Don’t Repeat Yourself” - a fundamental principle of software development that states: every piece of knowledge must have a single, unambiguous, authoritative representation within a system. In simpler terms: don’t write the same code (or logic) more than once.
The principle was formulated by Andy Hunt and Dave Thomas in their influential book “The Pragmatic Programmer” (1999). While it sounds simple, DRY is much more than just “don’t copy-paste code” - it’s about reducing duplication of knowledge and intent throughout your system.
Why DRY Matters
When you violate DRY, you create what developers call WET code - variously interpreted as “Write Everything Twice,” “We Enjoy Typing,” or “Waste Everyone’s Time.” WET code leads to:
- 🐛 More bugs - Fix a bug in one place, miss it in another
- ⏱️ Slower development - Change requirements? Update code in multiple places
- 😵 Higher cognitive load - Developers must remember to update all duplicates
- 🔧 Harder maintenance - The more repetition, the more that can go wrong
- 📚 Inconsistency - Duplicated code inevitably diverges over time
The True Cost of Duplication
Why WET code is expensive.
Imagine you have password validation logic duplicated in 5 places across your application. Then security requirements change (which they will!):
- You need to find all 5 places (assuming you remember them all)
- You need to update each one identically
- You need to test each one separately
- If you miss even one, you have a security vulnerability
With DRY code, you’d update one function and test it once. The savings multiply with every change over the lifetime of your code.
The Philosophy Behind DRY
DRY isn’t just a coding technique - it’s a mindset about how knowledge is organized in software.
graph TB
WC1[Code Block A<br/>Login Validation]
WC2[Code Block B<br/>Login Validation]
WC3[Code Block C<br/>Login Validation]
DS[Single Source<br/>Login Validation]
DC1[Usage Point A]
DC2[Usage Point B]
DC3[Usage Point C]
DC1 --> DS
DC2 --> DS
DC3 --> DS
WC1 -.->|Refactor to DRY| DS
style WC1 fill:#ffe6e6
style WC2 fill:#ffe6e6
style WC3 fill:#ffe6e6
style DS fill:#e6ffe6
style DC1 fill:#e6ffe6
style DC2 fill:#e6ffe6
style DC3 fill:#e6ffe6
Core Principles
Single Source of Truth
Every piece of knowledge exists in exactly one place.
When business logic, algorithms, or configuration exist in a single location, there’s no ambiguity about which version is “correct.” This is the foundation of maintainability.
Example: Instead of hardcoding tax rates in multiple services, store them in a central configuration.
Once and Only Once
Express each concept exactly once in your codebase.
This goes beyond literal code duplication. Even if the code looks different, if it represents the same knowledge or solves the same problem, it violates DRY.
Example: Two different validation functions that both check email format - they represent the same knowledge.
Knowledge Representation
DRY applies to logic, data, and documentation.
DRY isn’t just about code - it applies to:
- Business rules - Validation logic, calculations, workflows
- Data structures - Database schemas, API contracts
- Documentation - Comments should reference code, not duplicate it
- Configuration - Environment settings, feature flags
DRY in Context: Related Principles
DRY works best when balanced with other development principles:
| Principle | Focus | Relationship to DRY |
|---|---|---|
| DRY | Don’t repeat knowledge | Core principle - eliminate duplication |
| KISS | Keep solutions simple | DRY can violate KISS if over-abstracted |
| YAGNI | Don’t build premature features | Helps prevent premature DRY abstraction |
| Separation of Concerns | Each module has one responsibility | Enables DRY by organizing code logically |
Key Insight: DRY and KISS can conflict! Over-applying DRY leads to complex abstractions that violate KISS. The art is finding the right balance.
Levels of Duplication
Not all duplication is equal. Understanding the different levels helps you prioritize refactoring efforts.
1. Code Duplication
The most obvious form - identical or very similar code in multiple places.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ❌ WET: Code duplication
public class UserService {
public void createUser(String name, String email) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
// ... create user
}
public void updateUser(String name, String email) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
// ... update user
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ✅ DRY: Extracted validation
public class UserService {
public void createUser(String name, String email) {
validateUserInput(name, email);
// ... create user
}
public void updateUser(String name, String email) {
validateUserInput(name, email);
// ... update user
}
private void validateUserInput(String name, String email) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
}
}
2. Logic Duplication
Same logic implemented differently - harder to spot but equally problematic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ WET: Logic duplication (same business rule, different implementation)
public class OrderService {
public boolean canOrderBook(User user) {
return user.getAge() >= 18 || user.hasParentalConsent();
}
}
public class ReviewService {
public boolean canWriteReview(User user) {
// Same logic, different code!
if (user.getAge() < 18) {
return user.hasParentalConsent();
}
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ✅ DRY: Shared business rule
public class UserEligibilityService {
public boolean isEligibleForAdultContent(User user) {
return user.getAge() >= 18 || user.hasParentalConsent();
}
}
public class OrderService {
private UserEligibilityService eligibilityService;
public boolean canOrderBook(User user) {
return eligibilityService.isEligibleForAdultContent(user);
}
}
public class ReviewService {
private UserEligibilityService eligibilityService;
public boolean canWriteReview(User user) {
return eligibilityService.isEligibleForAdultContent(user);
}
}
classDiagram
class UserEligibilityService {
+isEligibleForAdultContent(User): boolean
}
class OrderService {
-eligibilityService: UserEligibilityService
+canOrderBook(User): boolean
}
class ReviewService {
-eligibilityService: UserEligibilityService
+canWriteReview(User): boolean
}
OrderService --> UserEligibilityService
ReviewService --> UserEligibilityService
note for UserEligibilityService "Single Source of Truth\nfor business rules"
3. Data Duplication
The same data stored or represented in multiple places.
1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ WET: Data duplication
public class Order {
private String productName;
private double productPrice;
private String productSku;
// Product data duplicated in Order
}
public class Product {
private String name;
private double price;
private String sku;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// ✅ DRY: Reference instead of duplication
public class Order {
private Product product; // Reference to single source of truth
private int quantity;
private LocalDateTime orderDate;
}
public class Product {
private Long id;
private String name;
private double price;
private String sku;
}
Note: Sometimes data duplication is intentional for performance (caching) or historical records (snapshots). The key is making it a conscious decision, not an accident.
4. Documentation Duplication
Comments that repeat what the code already says clearly.
1
2
3
4
5
6
7
8
9
10
11
12
// ❌ WET: Documentation duplicates code
public class Calculator {
/**
* Adds two numbers together and returns the sum
* @param a first number to add
* @param b second number to add
* @return the sum of a and b
*/
public int add(int a, int b) {
return a + b; // Add a and b and return the result
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ DRY: Documentation adds value, doesn't duplicate
public class PriceCalculator {
/**
* Calculates final price including tax and discounts.
* Tax is applied after discounts are deducted.
*
* @param basePrice original price before adjustments
* @param discountPercent discount rate (0-100)
* @param taxRate tax rate as decimal (e.g., 0.20 for 20%)
* @return final price with discounts and tax applied
*/
public double calculateFinalPrice(double basePrice, double discountPercent, double taxRate) {
double afterDiscount = basePrice * (1 - discountPercent / 100);
return afterDiscount * (1 + taxRate);
}
}
Common Duplication Patterns
Let’s explore real-world scenarios where duplication sneaks into codebases.
Pattern 1: Copy-Paste Code
The most common violation - copying code to make a “small change.”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ WET: Copy-paste programming
public class ReportGenerator {
public void generateUserReport(List<User> users) {
System.out.println("=== User Report ===");
System.out.println("Generated: " + LocalDateTime.now());
System.out.println("Total Records: " + users.size());
System.out.println("==================");
for (User user : users) {
System.out.println(user.getName() + " - " + user.getEmail());
}
}
public void generateProductReport(List<Product> products) {
System.out.println("=== Product Report ===");
System.out.println("Generated: " + LocalDateTime.now());
System.out.println("Total Records: " + products.size());
System.out.println("======================");
for (Product product : products) {
System.out.println(product.getName() + " - " + product.getPrice());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ✅ DRY: Generic report structure
public class ReportGenerator {
public <T> void generateReport(String title, List<T> items,
Function<T, String> formatter) {
System.out.println("=== " + title + " ===");
System.out.println("Generated: " + LocalDateTime.now());
System.out.println("Total Records: " + items.size());
System.out.println("=".repeat(title.length() + 8));
items.forEach(item -> System.out.println(formatter.apply(item)));
}
// Usage
public void printUserReport(List<User> users) {
generateReport("User Report", users,
user -> user.getName() + " - " + user.getEmail());
}
public void printProductReport(List<Product> products) {
generateReport("Product Report", products,
product -> product.getName() + " - " + product.getPrice());
}
}
Pattern 2: Similar Switch/If Statements
Multiple places checking the same conditions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ❌ WET: Duplicated switch logic
public class OrderProcessor {
public double calculateShipping(Order order) {
switch (order.getType()) {
case "STANDARD":
return 5.99;
case "EXPRESS":
return 12.99;
case "OVERNIGHT":
return 24.99;
default:
return 0.0;
}
}
public int getEstimatedDays(Order order) {
switch (order.getType()) {
case "STANDARD":
return 5;
case "EXPRESS":
return 2;
case "OVERNIGHT":
return 1;
default:
return 7;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ✅ DRY: Strategy pattern with data encapsulation
public enum ShippingType {
STANDARD(5.99, 5, "Standard Shipping"),
EXPRESS(12.99, 2, "Express Delivery"),
OVERNIGHT(24.99, 1, "Overnight Shipping");
private final double cost;
private final int deliveryDays;
private final String displayName;
ShippingType(double cost, int deliveryDays, String displayName) {
this.cost = cost;
this.deliveryDays = deliveryDays;
this.displayName = displayName;
}
public double getCost() { return cost; }
public int getDeliveryDays() { return deliveryDays; }
public String getDisplayName() { return displayName; }
}
public class OrderProcessor {
public double calculateShipping(Order order) {
return order.getShippingType().getCost();
}
public int getEstimatedDays(Order order) {
return order.getShippingType().getDeliveryDays();
}
}
graph TB
B1[calculateShipping<br/>switch statement]
B2[getEstimatedDays<br/>switch statement]
B3[getDisplayName<br/>switch statement]
A1[ShippingType Enum]
A2[getCost]
A3[getDeliveryDays]
A4[getDisplayName]
A1 --> A2
A1 --> A3
A1 --> A4
B1 -.->|Refactor| A1
style B1 fill:#ffe6e6
style B2 fill:#ffe6e6
style B3 fill:#ffe6e6
style A1 fill:#e6ffe6
style A2 fill:#e6ffe6
style A3 fill:#e6ffe6
style A4 fill:#e6ffe6
Pattern 3: Magic Numbers and Strings
Hardcoded values repeated throughout code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ❌ WET: Magic numbers everywhere
public class SubscriptionService {
public boolean isEligibleForPremium(User user) {
return user.getAccountAge() >= 30; // 30 days
}
public void sendWelcomeEmail(User user) {
if (user.getAccountAge() < 30) { // Same 30, different context
// Send trial info
}
}
public double getDiscount(User user) {
if (user.getAccountAge() >= 30) { // Again 30!
return 0.10; // 10% discount
}
return 0.0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ✅ DRY: Named constants with clear meaning
public class SubscriptionService {
private static final int TRIAL_PERIOD_DAYS = 30;
private static final double PREMIUM_MEMBER_DISCOUNT = 0.10;
public boolean isEligibleForPremium(User user) {
return user.getAccountAge() >= TRIAL_PERIOD_DAYS;
}
public void sendWelcomeEmail(User user) {
if (user.getAccountAge() < TRIAL_PERIOD_DAYS) {
// Send trial info
}
}
public double getDiscount(User user) {
if (user.getAccountAge() >= TRIAL_PERIOD_DAYS) {
return PREMIUM_MEMBER_DISCOUNT;
}
return 0.0;
}
}
Pro Tip: If you need to change a number in more than one place, it should be a constant!
When DRY Makes Sense (And When It Doesn’t)
DRY is powerful, but like all principles, it can be misapplied. Let’s explore the nuances.
✅ Apply DRY When:
1. Same Business Logic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// ✅ DRY is appropriate - same business rule
public class PaymentValidator {
private static final double MIN_PAYMENT_AMOUNT = 0.01;
public void validateCheckoutPayment(double amount) {
if (amount < MIN_PAYMENT_AMOUNT) {
throw new InvalidPaymentException("Minimum payment is $0.01");
}
}
public void validateRefundAmount(double amount) {
if (amount < MIN_PAYMENT_AMOUNT) {
throw new InvalidPaymentException("Minimum refund is $0.01");
}
}
}
// Better: Extract the validation
public class PaymentValidator {
private static final double MIN_PAYMENT_AMOUNT = 0.01;
private void validateAmount(double amount, String operation) {
if (amount < MIN_PAYMENT_AMOUNT) {
throw new InvalidPaymentException(
String.format("Minimum %s is $%.2f", operation, MIN_PAYMENT_AMOUNT)
);
}
}
public void validateCheckoutPayment(double amount) {
validateAmount(amount, "payment");
}
public void validateRefundAmount(double amount) {
validateAmount(amount, "refund");
}
}
2. Repeated Algorithms
1
2
3
4
5
6
7
// ✅ DRY is appropriate - same algorithm
public class StringUtils {
// Use this one reusable method everywhere
public static boolean isNullOrEmpty(String str) {
return str == null || str.trim().isEmpty();
}
}
3. Configuration and Constants
1
2
3
4
5
6
// ✅ DRY is appropriate - single source for configuration
public class AppConfig {
public static final int MAX_LOGIN_ATTEMPTS = 3;
public static final int SESSION_TIMEOUT_MINUTES = 30;
public static final String DEFAULT_LOCALE = "en_US";
}
❌ Avoid DRY When:
1. Coincidental Similarity
Sometimes code looks similar by coincidence, not because it represents the same knowledge.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ❌ DON'T apply DRY - these are different concepts
public class UserService {
public void validateUser(User user) {
if (user.getAge() < 18) { // Business rule: age restriction
throw new ValidationException("Must be 18+");
}
}
}
public class ReportFilter {
public List<User> filterAdults(List<User> users) {
return users.stream()
.filter(user -> user.getAge() >= 18) // Data filtering, not validation
.collect(Collectors.toList());
}
}
// These both check age >= 18, but for different reasons!
// Extracting to a shared method would create false coupling.
Warning: Premature abstraction is worse than duplication. If you’re not sure whether code represents the same knowledge, keep it separate until the pattern becomes clear.
2. Different Rates of Change
Code that changes for different reasons shouldn’t be DRYed together.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌ DON'T apply DRY - different change drivers
public class PricingEngine {
// Changes when business rules change
private double calculateBusinessDiscount(Order order) {
return order.getTotal() * 0.10;
}
// Changes when promotions change
private double calculatePromotionalDiscount(Order order) {
return order.getTotal() * 0.10;
}
}
// Even though both calculate 10%, they exist for different reasons
// and will likely diverge. Keep them separate!
This concept comes from the Single Responsibility Principle: code that changes for different reasons should be separated.
3. Crossing Bounded Contexts
In larger systems, duplication across bounded contexts is often acceptable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✅ Acceptable duplication - different contexts
// User Service Context
public class User {
private String email;
private String name;
// User identity and authentication
}
// Order Service Context
public class CustomerInfo {
private String email;
private String name;
// Customer information for orders
}
// These look similar but serve different purposes in different contexts.
// Forcing them to be the same creates tight coupling between services.
graph TB
U1[User Model<br/>email, name, password]
U2[Authentication Logic]
O1[CustomerInfo Model<br/>email, name, shipping]
O2[Order Processing Logic]
note1[Different contexts<br/>Different purposes<br/>Different change drivers]
style U1 fill:#e6f3ff
style U2 fill:#e6f3ff
style O1 fill:#fff3e6
style O2 fill:#fff3e6
style note1 fill:#fff3cd
Refactoring to DRY: Step by Step
Let’s walk through a complete refactoring example.
Before: WET Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class NotificationService {
public void sendEmailNotification(User user, String message) {
// Email setup
String to = user.getEmail();
String subject = "Notification";
String body = "Hello " + user.getName() + ",\n\n" + message;
// Validation
if (to == null || to.isEmpty()) {
System.err.println("Invalid email address");
return;
}
// Logging
System.out.println("Sending email to: " + to);
System.out.println("Subject: " + subject);
// Send (simulated)
EmailClient.send(to, subject, body);
// Success logging
System.out.println("Email sent successfully");
}
public void sendSmsNotification(User user, String message) {
// SMS setup
String phoneNumber = user.getPhoneNumber();
String body = "Hello " + user.getName() + ", " + message;
// Validation
if (phoneNumber == null || phoneNumber.isEmpty()) {
System.err.println("Invalid phone number");
return;
}
// Logging
System.out.println("Sending SMS to: " + phoneNumber);
// Send (simulated)
SmsClient.send(phoneNumber, body);
// Success logging
System.out.println("SMS sent successfully");
}
public void sendPushNotification(User user, String message) {
// Push setup
String deviceId = user.getDeviceId();
String body = "Hello " + user.getName() + ", " + message;
// Validation
if (deviceId == null || deviceId.isEmpty()) {
System.err.println("Invalid device ID");
return;
}
// Logging
System.out.println("Sending push notification to: " + deviceId);
// Send (simulated)
PushClient.send(deviceId, body);
// Success logging
System.out.println("Push notification sent successfully");
}
}
After: DRY Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// Step 1: Create abstraction for notification channels
public interface NotificationChannel {
String getChannelName();
String getRecipient(User user);
void send(String recipient, String message);
}
// Step 2: Implement concrete channels
public class EmailChannel implements NotificationChannel {
@Override
public String getChannelName() {
return "Email";
}
@Override
public String getRecipient(User user) {
return user.getEmail();
}
@Override
public void send(String recipient, String message) {
EmailClient.send(recipient, "Notification", message);
}
}
public class SmsChannel implements NotificationChannel {
@Override
public String getChannelName() {
return "SMS";
}
@Override
public String getRecipient(User user) {
return user.getPhoneNumber();
}
@Override
public void send(String recipient, String message) {
SmsClient.send(recipient, message);
}
}
public class PushChannel implements NotificationChannel {
@Override
public String getChannelName() {
return "Push";
}
@Override
public String getRecipient(User user) {
return user.getDeviceId();
}
@Override
public void send(String recipient, String message) {
PushClient.send(recipient, message);
}
}
// Step 3: Create unified service with common logic
public class NotificationService {
private final Logger logger = LoggerFactory.getLogger(NotificationService.class);
public void sendNotification(User user, String message, NotificationChannel channel) {
// Common validation
String recipient = channel.getRecipient(user);
if (recipient == null || recipient.isEmpty()) {
logger.error("Invalid recipient for {}", channel.getChannelName());
return;
}
// Common logging
logger.info("Sending {} to: {}", channel.getChannelName(), recipient);
// Common message formatting
String body = formatMessage(user, message);
// Send via channel
try {
channel.send(recipient, body);
logger.info("{} sent successfully", channel.getChannelName());
} catch (Exception e) {
logger.error("Failed to send {}: {}", channel.getChannelName(), e.getMessage());
}
}
private String formatMessage(User user, String message) {
return "Hello " + user.getName() + ", " + message;
}
// Convenience methods
public void sendEmail(User user, String message) {
sendNotification(user, message, new EmailChannel());
}
public void sendSms(User user, String message) {
sendNotification(user, message, new SmsChannel());
}
public void sendPush(User user, String message) {
sendNotification(user, message, new PushChannel());
}
}
Benefits of Refactoring
| Aspect | Before (WET) | After (DRY) |
|---|---|---|
| Lines of Code | ~60 lines | ~40 lines (with interfaces) |
| Bug Fix | Fix in 3 places | Fix in 1 place |
| New Channel | Copy-paste entire method | Implement one interface |
| Logging Change | Update 3 methods | Update 1 method |
| Testability | 3 methods to test | 1 method + 3 simple implementations |
graph TB
B1[sendEmailNotification<br/>60 lines]
B2[sendSmsNotification<br/>60 lines]
B3[sendPushNotification<br/>60 lines]
A1[NotificationChannel<br/>Interface]
A2[sendNotification<br/>Common Logic]
A3[EmailChannel]
A4[SmsChannel]
A5[PushChannel]
A3 --> A1
A4 --> A1
A5 --> A1
A1 --> A2
B1 -.->|Refactor| A2
style B1 fill:#ffe6e6
style B2 fill:#ffe6e6
style B3 fill:#ffe6e6
style A1 fill:#e6ffe6
style A2 fill:#d4edda
style A3 fill:#e6ffe6
style A4 fill:#e6ffe6
style A5 fill:#e6ffe6
Common DRY Pitfalls
Even experienced developers fall into these traps.
Pitfall 1: Premature Abstraction
"Duplication is far cheaper than the wrong abstraction."
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ Premature abstraction - trying to DRY too early
public class DataProcessor {
// After seeing two similar methods, a junior dev might create:
public <T> List<T> processData(List<T> data, Predicate<T> filter,
Function<T, T> transformer,
Comparator<T> sorter) {
return data.stream()
.filter(filter)
.map(transformer)
.sorted(sorter)
.collect(Collectors.toList());
}
}
// Problem: This generic method is harder to understand and use than
// two specific methods would be. You've made it "DRY" but not better.
Pro Tip: Follow the “Rule of Three” - wait until you see duplication three times before abstracting. This gives you enough examples to identify the real pattern.
Pitfall 2: Over-Engineering
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ❌ Over-engineered DRY - more complex than the problem
public class FieldValidator {
private Map<String, List<ValidationRule>> rules = new HashMap<>();
private Map<String, ValidationErrorHandler> errorHandlers = new HashMap<>();
public void registerRule(String field, ValidationRule rule) { }
public void registerErrorHandler(String field, ValidationErrorHandler handler) { }
public ValidationResult validate(String field, Object value) { }
// ... 100 more lines
}
// ✅ Simple approach - good enough for most cases
public class UserValidator {
public void validateEmail(String email) {
if (email == null || !email.contains("@")) {
throw new ValidationException("Invalid email");
}
}
public void validateAge(int age) {
if (age < 0 || age > 150) {
throw new ValidationException("Invalid age");
}
}
}
This violates KISS (Keep It Simple, Stupid). Sometimes simple duplication is better than complex abstraction.
Pitfall 3: False Unification
Combining things that are similar now but will diverge later.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ❌ False unification - these will diverge
public class AddressFormatter {
public String formatAddress(Address address, String type) {
if (type.equals("shipping")) {
// Shipping addresses need delivery instructions
return address.getStreet() + ", " + address.getCity();
} else if (type.equals("billing")) {
// Billing addresses need tax jurisdiction
return address.getStreet() + ", " + address.getCity();
}
// As requirements evolve, these formats will diverge
}
}
// ✅ Separate formatters - allows independent evolution
public class ShippingAddressFormatter {
public String format(Address address) {
return String.format("%s\n%s\n%s",
address.getStreet(),
address.getCity(),
address.getDeliveryInstructions());
}
}
public class BillingAddressFormatter {
public String format(Address address) {
return String.format("%s\n%s\nTax Zone: %s",
address.getStreet(),
address.getCity(),
address.getTaxJurisdiction());
}
}
Practical DRY Techniques
Here are concrete techniques you can apply today.
1. Extract Method
The simplest refactoring - pull duplicate code into a method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Before
public class OrderService {
public void processOrder(Order order) {
double total = 0;
for (OrderItem item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
order.setTotal(total);
}
public double estimateOrder(Order order) {
double total = 0;
for (OrderItem item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
return total;
}
}
// After
public class OrderService {
public void processOrder(Order order) {
order.setTotal(calculateTotal(order));
}
public double estimateOrder(Order order) {
return calculateTotal(order);
}
private double calculateTotal(Order order) {
return order.getItems().stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
}
}
2. Use Inheritance/Composition
For related classes with common behavior.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// DRY through inheritance
public abstract class BaseRepository<T> {
protected void logOperation(String operation, T entity) {
System.out.println(operation + ": " + entity);
}
public abstract void save(T entity);
public abstract T findById(Long id);
}
public class UserRepository extends BaseRepository<User> {
@Override
public void save(User user) {
logOperation("Saving user", user);
// Save logic
}
@Override
public User findById(Long id) {
// Find logic
return null;
}
}
3. Configuration Over Code
Move repeated values to configuration.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Before: Hardcoded in multiple places
public class EmailService {
public void sendWelcome(User user) {
send(user.getEmail(), "welcome@example.com", "Welcome!");
}
public void sendReset(User user) {
send(user.getEmail(), "welcome@example.com", "Reset Password");
}
}
// After: Configuration-driven
public class EmailService {
@Value("${email.from}")
private String fromAddress;
public void sendWelcome(User user) {
send(user.getEmail(), fromAddress, "Welcome!");
}
public void sendReset(User user) {
send(user.getEmail(), fromAddress, "Reset Password");
}
}
4. Template Method Pattern
For algorithms with common structure but varying steps.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public abstract class DataImporter {
// Template method - defines the algorithm
public final void importData(String source) {
validateSource(source);
String data = readData(source);
Object parsed = parseData(data);
saveToDatabase(parsed);
logSuccess(source);
}
// Common implementations
private void validateSource(String source) {
if (source == null || source.isEmpty()) {
throw new IllegalArgumentException("Invalid source");
}
}
private void logSuccess(String source) {
System.out.println("Successfully imported from: " + source);
}
// Subclasses implement these
protected abstract String readData(String source);
protected abstract Object parseData(String data);
protected abstract void saveToDatabase(Object data);
}
public class CsvImporter extends DataImporter {
@Override
protected String readData(String source) {
// CSV reading logic
return "csv data";
}
@Override
protected Object parseData(String data) {
// CSV parsing logic
return new Object();
}
@Override
protected void saveToDatabase(Object data) {
// Save logic
}
}
DRY in Practice: Real-World Example
Let’s see a complete example from a real application.
Scenario: E-Commerce Pricing System
An e-commerce platform needs to calculate prices with various rules:
- Base price
- Member discounts
- Seasonal promotions
- Bulk order discounts
- Tax calculation
Initial (WET) Implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class PricingService {
public double calculateCheckoutPrice(Order order) {
double total = 0;
for (OrderItem item : order.getItems()) {
double price = item.getProduct().getBasePrice();
// Apply member discount
if (order.getUser().isMember()) {
price *= 0.9; // 10% off
}
// Apply seasonal discount
if (isHolidaySeason()) {
price *= 0.95; // 5% off
}
// Apply bulk discount
if (item.getQuantity() >= 10) {
price *= 0.85; // 15% off
}
total += price * item.getQuantity();
}
// Add tax
total *= 1.2; // 20% tax
return total;
}
public double calculateQuotePrice(Quote quote) {
double total = 0;
for (QuoteItem item : quote.getItems()) {
double price = item.getProduct().getBasePrice();
// Apply member discount (duplicate!)
if (quote.getUser().isMember()) {
price *= 0.9;
}
// Apply seasonal discount (duplicate!)
if (isHolidaySeason()) {
price *= 0.95;
}
// Apply bulk discount (duplicate!)
if (item.getQuantity() >= 10) {
price *= 0.85;
}
total += price * item.getQuantity();
}
// Add tax (duplicate!)
total *= 1.2;
return total;
}
private boolean isHolidaySeason() {
// Check if current date is in holiday season
return false;
}
}
DRY Refactoring
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Step 1: Model discounts as objects
public interface PricingRule {
String getName();
double apply(double price, PricingContext context);
}
public class PricingContext {
private User user;
private int quantity;
private Product product;
// Constructor and getters
public PricingContext(User user, int quantity, Product product) {
this.user = user;
this.quantity = quantity;
this.product = product;
}
public User getUser() { return user; }
public int getQuantity() { return quantity; }
public Product getProduct() { return product; }
}
// Step 2: Implement specific rules
public class MemberDiscountRule implements PricingRule {
private static final double MEMBER_DISCOUNT = 0.10;
@Override
public String getName() {
return "Member Discount";
}
@Override
public double apply(double price, PricingContext context) {
if (context.getUser().isMember()) {
return price * (1 - MEMBER_DISCOUNT);
}
return price;
}
}
public class SeasonalDiscountRule implements PricingRule {
private static final double SEASONAL_DISCOUNT = 0.05;
@Override
public String getName() {
return "Seasonal Discount";
}
@Override
public double apply(double price, PricingContext context) {
if (isHolidaySeason()) {
return price * (1 - SEASONAL_DISCOUNT);
}
return price;
}
private boolean isHolidaySeason() {
Month currentMonth = LocalDate.now().getMonth();
return currentMonth == Month.NOVEMBER || currentMonth == Month.DECEMBER;
}
}
public class BulkDiscountRule implements PricingRule {
private static final int BULK_THRESHOLD = 10;
private static final double BULK_DISCOUNT = 0.15;
@Override
public String getName() {
return "Bulk Discount";
}
@Override
public double apply(double price, PricingContext context) {
if (context.getQuantity() >= BULK_THRESHOLD) {
return price * (1 - BULK_DISCOUNT);
}
return price;
}
}
public class TaxRule implements PricingRule {
private static final double TAX_RATE = 0.20;
@Override
public String getName() {
return "Tax";
}
@Override
public double apply(double price, PricingContext context) {
return price * (1 + TAX_RATE);
}
}
// Step 3: Create unified pricing engine
public class PricingEngine {
private final List<PricingRule> discountRules;
private final List<PricingRule> taxRules;
public PricingEngine() {
this.discountRules = Arrays.asList(
new MemberDiscountRule(),
new SeasonalDiscountRule(),
new BulkDiscountRule()
);
this.taxRules = Arrays.asList(
new TaxRule()
);
}
public double calculatePrice(Product product, int quantity, User user) {
PricingContext context = new PricingContext(user, quantity, product);
double price = product.getBasePrice();
// Apply discount rules
for (PricingRule rule : discountRules) {
price = rule.apply(price, context);
}
// Calculate line total
double lineTotal = price * quantity;
// Apply tax rules
for (PricingRule rule : taxRules) {
lineTotal = rule.apply(lineTotal, context);
}
return lineTotal;
}
}
// Step 4: Simplified service
public class PricingService {
private final PricingEngine engine = new PricingEngine();
public double calculateCheckoutPrice(Order order) {
return order.getItems().stream()
.mapToDouble(item -> engine.calculatePrice(
item.getProduct(),
item.getQuantity(),
order.getUser()
))
.sum();
}
public double calculateQuotePrice(Quote quote) {
return quote.getItems().stream()
.mapToDouble(item -> engine.calculatePrice(
item.getProduct(),
item.getQuantity(),
quote.getUser()
))
.sum();
}
}
Benefits of This Refactoring
✅ Single Source of Truth - Each discount rule exists in exactly one place
✅ Easy to Test - Each rule can be tested independently
✅ Easy to Extend - Add new rules by implementing PricingRule
✅ Easy to Modify - Change a discount percentage in one place
✅ Self-Documenting - Rule names make the code readable
classDiagram
class PricingRule {
<<interface>>
+getName(): String
+apply(price, context): double
}
class PricingEngine {
-discountRules: List~PricingRule~
-taxRules: List~PricingRule~
+calculatePrice(product, quantity, user): double
}
class MemberDiscountRule {
+apply(price, context): double
}
class SeasonalDiscountRule {
+apply(price, context): double
}
class BulkDiscountRule {
+apply(price, context): double
}
class TaxRule {
+apply(price, context): double
}
class PricingService {
-engine: PricingEngine
+calculateCheckoutPrice(Order): double
+calculateQuotePrice(Quote): double
}
PricingRule <|.. MemberDiscountRule
PricingRule <|.. SeasonalDiscountRule
PricingRule <|.. BulkDiscountRule
PricingRule <|.. TaxRule
PricingEngine --> PricingRule
PricingService --> PricingEngine
DRY Checklist
Use this checklist when reviewing code for DRY violations:
Code Review Questions
- Are there identical or very similar code blocks?
- Are the same magic numbers/strings used in multiple places?
- Do multiple methods implement the same business logic?
- Is the same validation logic repeated?
- Are switch/if statements checking the same conditions in multiple places?
- Does documentation repeat what the code clearly states?
Refactoring Questions
- Does this duplication represent the same knowledge?
- Will these duplicated parts change for the same reasons?
- Am I creating an abstraction too early (Rule of Three)?
- Will this abstraction make the code simpler or more complex?
- Does this violate KISS or YAGNI?
Design Questions
- Can I extract a method/function?
- Can I use inheritance or composition?
- Should this be a configuration value instead of code?
- Would a design pattern help here (Strategy, Template Method)?
- Is this duplication across bounded contexts (acceptable)?
Key Takeaways
🎯 DRY is about knowledge, not just code - Even different-looking code can violate DRY if it represents the same knowledge
⚖️ Balance is key - DRY should be balanced with KISS and YAGNI. Sometimes duplication is better than the wrong abstraction
🔢 Follow the Rule of Three - Wait until you see duplication three times before abstracting
🎨 Different contexts, different code - Duplication across bounded contexts or different change drivers is often acceptable
🧪 DRY makes testing easier - One place to test instead of many
📈 Start simple, refactor later - It’s easier to remove duplication than to remove wrong abstractions
Remember: The goal isn’t to eliminate all duplication at any cost. The goal is to create maintainable, understandable code. DRY is a tool to achieve that, not an end in itself.
Conclusion
The DRY principle is one of the most powerful tools in your software engineering toolkit. By eliminating duplication of knowledge, you create code that’s easier to maintain, test, and evolve. However, like all principles, it must be applied judiciously.
Key points to remember:
- DRY is about knowledge, not just code syntax
- Balance DRY with KISS - don’t over-abstract
- Wait for patterns to emerge before abstracting (Rule of Three)
- Recognize when duplication is acceptable - different contexts, different rates of change
- Use appropriate techniques - extract methods, inheritance, configuration, patterns
As you continue your journey as a software engineer, you’ll develop intuition for when to apply DRY and when to keep things separate. Start by identifying obvious duplication, practice refactoring techniques, and always ask: “Does this duplication represent the same knowledge?”
Next Steps: Now that you understand DRY, explore how it combines with KISS and YAGNI to create a complete philosophy of pragmatic software development!
Further Reading
- “The Pragmatic Programmer” by Andy Hunt and Dave Thomas - The original source of DRY
- “Refactoring: Improving the Design of Existing Code” by Martin Fowler - Practical refactoring techniques
- “Clean Code” by Robert C. Martin - More principles for better code
- Design Patterns - Reusable solutions to common problems
