DRY vs WET vs AHA
When to Use
When deciding whether to abstract duplicated code or let it remain duplicated, especially early in a feature's lifecycle.
Decision Framework
| If you... | Use | Why |
|---|---|---|
| Know the abstraction is correct (third+ instance, stable requirements) | DRY | Eliminates knowledge duplication, single source of truth |
| Have only 1-2 instances, requirements still evolving | WET (Write Everything Twice) | Preserves flexibility, prevents premature abstraction |
| See similar code but unsure if it represents same knowledge | AHA (Avoid Hasty Abstractions) | Wait for clarity, let patterns emerge naturally |
| Existing abstraction is wrong and causing pain | WET -> DRY | Inline duplication, understand variance, re-abstract correctly |
| Duplication is incidental (similar syntax, different intent) | WET | Forced unification would couple unrelated concepts |
Philosophy Comparison
DRY (Don't Repeat Yourself)
- Goal: Every piece of knowledge has single representation
- Strength: Eliminates knowledge duplication, easier maintenance when correct
- Weakness: Premature or incorrect abstractions couple unrelated code
- Best for: Stable requirements, well-understood domains
WET (Write Everything Twice)
- Goal: Duplicate code 1-2 times before abstracting
- Strength: Keeps code flexible while requirements evolve
- Weakness: Can lead to actual knowledge duplication if never refactored
- Best for: New features, exploratory development, rapid prototyping
AHA (Avoid Hasty Abstractions)
- Goal: Prefer duplication over wrong abstraction, abstract when obvious
- Strength: Emphasizes understanding before abstracting
- Weakness: Requires discipline to recognize "right" moment
- Best for: Ongoing maintenance, balancing DRY and WET
Pattern
// SCENARIO: Building a notification system
// ITERATION 1: First use case (email notifications)
function sendEmailNotification(user, message) {
const email = buildEmail(user.email, message);
smtp.send(email);
log.info(`Email sent to ${user.email}`);
}
// ITERATION 2: Second use case (SMS notifications) - WET approach
function sendSmsNotification(user, message) {
const sms = buildSms(user.phone, message);
smsApi.send(sms);
log.info(`SMS sent to ${user.phone}`);
}
// DON'T abstract yet — let requirements emerge
// ITERATION 3: Third use case (push notifications) - AHA moment
// NOW the pattern is clear: different channels, same flow
// Abstract with confidence:
interface NotificationChannel {
send(recipient: string, message: string): Promise<void>;
format(recipient: string, message: string): any;
}
class EmailChannel implements NotificationChannel {
async send(email: string, message: string) {
const formatted = this.format(email, message);
await smtp.send(formatted);
}
format(email: string, message: string) {
return buildEmail(email, message);
}
}
class SmsChannel implements NotificationChannel { /* ... */ }
class PushChannel implements NotificationChannel { /* ... */ }
// Generic notification sender (DRY)
async function sendNotification(
channel: NotificationChannel,
recipient: string,
message: string
) {
await channel.send(recipient, message);
log.info(`Notification sent via ${channel.constructor.name}`);
}
Common Mistakes
- Abstracting at instance #1 — No evidence the abstraction fits multiple use cases; likely to be wrong
- Never refactoring WET code — Duplication becomes entrenched, leads to divergence and bugs
- Mistaking similar syntax for same knowledge — Code looks similar but serves different business purposes
- Treating AHA as "never abstract" — AHA means "wait for the right moment," not "avoid abstraction forever"
- Ignoring the pain signal — If changing duplicated code is painful, it's time to abstract (even if only 2 instances)