Over-DRY Anti-Patterns
When to Use
When evaluating whether an abstraction is helping or hurting, and when to choose duplication over the wrong abstraction.
Common Over-DRY Anti-Patterns
| Anti-Pattern | What It Looks Like | Why It's Harmful | Solution |
|---|---|---|---|
| Premature Abstraction | Creating abstractions before understanding variance | Wrong abstractions hard to change, couple unrelated code | Wait for Rule of Three, use WET/AHA |
| God Objects | Single class/module handles everything "to avoid duplication" | Violates Single Responsibility, hard to test, high coupling | Split by responsibility, accept some duplication |
| Forced Unification | Merging similar but not identical concepts | Complexity grows as special cases accumulate | Keep separate if they serve different purposes |
| Leaky Abstractions | Abstraction exposes implementation details | Defeats purpose, creates coupling | Redesign abstraction or accept duplication |
| Coupling Through DRY | Shared state/schema couples independent modules | Changes ripple across unrelated code | Use separate models, accept some duplication |
| Abstraction Layers | Too many layers of indirection | Cognitive overhead, hard to debug | Inline unnecessary layers |
Anti-Pattern: Premature Abstraction
# ANTI-PATTERN: Abstract after seeing TWO similar functions
def send_email(to, subject, body):
# ...
pass
def send_sms(to, message):
# ...
pass
# Premature abstraction (only 2 instances!)
class Notifier:
def __init__(self, channel_type, recipient_field):
self.channel_type = channel_type
self.recipient_field = recipient_field
def send(self, recipient, **kwargs):
if self.channel_type == 'email':
self._send_email(recipient, kwargs['subject'], kwargs['body'])
elif self.channel_type == 'sms':
self._send_sms(recipient, kwargs['message'])
# What about push? Slack? Discord? This abstraction is already awkward.
# BETTER: Wait for third instance, then abstract with full understanding
# Keep send_email and send_sms separate until pattern is clear
Anti-Pattern: God Object
// ANTI-PATTERN: Everything in one class to "avoid duplication"
class UserManager {
// User CRUD
createUser(data) { /* ... */ }
updateUser(id, data) { /* ... */ }
deleteUser(id) { /* ... */ }
// Authentication (should be separate)
login(email, password) { /* ... */ }
logout(userId) { /* ... */ }
// Authorization (should be separate)
hasPermission(userId, permission) { /* ... */ }
// Profile (should be separate)
updateAvatar(userId, image) { /* ... */ }
// Email (definitely should be separate)
sendWelcomeEmail(userId) { /* ... */ }
sendPasswordReset(userId) { /* ... */ }
}
// BETTER: Split by responsibility
class UserRepository { /* CRUD */ }
class AuthService { /* Authentication */ }
class AuthorizationService { /* Permissions */ }
class UserProfileService { /* Profile operations */ }
class UserNotificationService { /* Emails */ }
// Yes, more classes. But each is focused, testable, and maintainable.
Anti-Pattern: Coupling Through Shared Schema
// ANTI-PATTERN: Frontend, backend, database share exact same schema
// Seems DRY, but creates hidden coupling
// shared-schema.ts
interface User {
id: number;
email: string;
passwordHash: string; // Sensitive! Should NOT be in frontend
internalNotes: string; // Admin-only, not for API
createdAt: Date;
lastLoginIp: string; // Privacy concern
}
// Now frontend, backend, database all use same User type
// Problem: Any change affects ALL layers
// Security problem: Frontend sees passwordHash field
// Privacy problem: API returns lastLoginIp
// BETTER: Separate models per boundary
// database/models.ts
interface UserEntity {
id: number;
email: string;
passwordHash: string;
internalNotes: string;
createdAt: Date;
lastLoginIp: string;
}
// api/dto.ts
interface UserDto {
id: number;
email: string;
createdAt: string; // Serialized differently
// No passwordHash, no internalNotes, no lastLoginIp
}
// frontend/types.ts
interface User {
id: number;
email: string;
createdAt: Date;
}
// Mappers between layers (small duplication, huge benefit)
function toDto(entity: UserEntity): UserDto {
return {
id: entity.id,
email: entity.email,
createdAt: entity.createdAt.toISOString(),
};
}
Anti-Pattern: Over-Generalization
// ANTI-PATTERN: Generic abstraction that handles too many cases
public class DataProcessor<T, R, C extends ProcessingContext> {
private Validator<T> validator;
private Transformer<T, R> transformer;
private Persister<R, C> persister;
public R process(T input, C context) throws ProcessingException {
if (!validator.validate(input, context.getValidationRules())) {
throw new ValidationException();
}
R transformed = transformer.transform(input, context.getTransformOptions());
persister.persist(transformed, context);
return transformed;
}
}
// Usage is incredibly complex
DataProcessor<UserInput, UserEntity, UserProcessingContext> processor =
new DataProcessor<>(
new UserValidator(),
new UserTransformer(),
new UserPersister()
);
// BETTER: Specific classes that are obvious
public class UserService {
public User createUser(CreateUserRequest request) {
validateUserInput(request);
User user = buildUserFromRequest(request);
repository.save(user);
return user;
}
}
The Wrong Abstraction Cost
From Sandi Metz:
- Developer sees duplication
- Developer creates abstraction to eliminate duplication
- New requirement emerges that doesn't quite fit abstraction
- Developer adds conditional to abstraction to handle new case
- Repeat step 3-4 multiple times
- Abstraction becomes complex, brittle, hard to understand
- Cost of wrong abstraction > cost of duplication
Solution: Prefer duplication over wrong abstraction. When abstraction is wrong:
- Re-introduce duplication by inlining abstraction into callers
- Delete the abstraction
- Let new pattern emerge naturally
- Abstract again when pattern is clear
Common Mistakes
- Worshipping DRY as absolute rule — Context and judgment matter; sometimes duplication is better
- Confusing DRY with code minimization — DRY is about knowledge, not lines of code
- Creating abstractions too early — Without understanding variance, abstractions will be wrong
- Never revisiting abstractions — Wrong abstractions accumulate; refactor when painful
- Sharing code across boundaries that should be independent — Microservices, frontend/backend need separate models