DRY, KISS, YAGNI — The Holy Trinity
Master the three fundamental principles every developer should know: Don't Repeat Yourself, Keep It Simple Stupid, and You Aren't Gonna Need It.
Three acronyms that every developer encounters early in their career, yet many misunderstand or misapply. DRY, KISS, and YAGNI form the foundation of pragmatic software development. Let’s explore what they really mean — and when they can be taken too far.
DRY — Don’t Repeat Yourself
The DRY principle states: “Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.” — Andy Hunt & Dave Thomas, The Pragmatic Programmer.
Notice it says knowledge, not code. This distinction is critical.
What DRY Really Means
// ❌ Duplication of knowledge — the validation rules exist in two places
function validateCreateUser(data: CreateUserDTO) {
if (!data.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
throw new Error('Invalid email');
}
if (data.name.length < 2 || data.name.length > 100) {
throw new Error('Name must be 2-100 characters');
}
}
function validateUpdateUser(data: UpdateUserDTO) {
if (data.email && !data.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
throw new Error('Invalid email');
}
if (data.name && (data.name.length < 2 || data.name.length > 100)) {
throw new Error('Name must be 2-100 characters');
}
}
// ✅ Single source of truth for validation rules
const userRules = {
email: (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
name: (value: string) => value.length >= 2 && value.length <= 100,
};
function validateFields(
data: Record<string, unknown>,
rules: Record<string, (v: any) => boolean>,
requiredFields: string[] = [],
) {
for (const [field, value] of Object.entries(data)) {
if (rules[field] && !rules[field](value)) {
throw new ValidationError(`Invalid ${field}`);
}
}
for (const field of requiredFields) {
if (!(field in data)) {
throw new ValidationError(`${field} is required`);
}
}
}
When DRY Goes Wrong
Not all similar-looking code is actual duplication. Sometimes two pieces of code look identical but represent different concepts that will evolve independently.
// These look similar, but they serve different domains
// and will change for different reasons
// Billing address validation
function validateBillingAddress(addr: BillingAddress) {
return addr.street.length > 0 && addr.zip.match(/^\d{5}$/);
}
// Shipping address validation
function validateShippingAddress(addr: ShippingAddress) {
return addr.street.length > 0 && addr.zip.match(/^\d{5}$/);
}
// DON'T merge these just because they look similar!
// Billing might later require tax ID validation.
// Shipping might need delivery zone checks.
The rule of three: Wait until you see duplication three times before extracting. Two occurrences might be coincidental similarity.
KISS — Keep It Simple, Stupid
The simplest solution that works is usually the best. Complexity is the enemy of reliability.
Over-Engineering in Action
// ❌ Astronomically over-engineered for a simple task
interface IStringTransformerStrategy {
transform(input: string): string;
}
class UpperCaseTransformer implements IStringTransformerStrategy {
transform(input: string): string { return input.toUpperCase(); }
}
class StringTransformerFactory {
create(type: 'upper' | 'lower'): IStringTransformerStrategy {
switch (type) {
case 'upper': return new UpperCaseTransformer();
default: throw new Error('Unknown type');
}
}
}
class StringTransformationService {
constructor(private factory: StringTransformerFactory) {}
execute(input: string, type: 'upper' | 'lower'): string {
const transformer = this.factory.create(type);
return transformer.transform(input);
}
}
// Just to do this:
const result = new StringTransformationService(
new StringTransformerFactory()
).execute('hello', 'upper');
// ✅ KISS version
const result = 'hello'.toUpperCase();
KISS Doesn’t Mean Dumb
KISS doesn’t mean avoiding all abstraction. It means choosing the right level of abstraction for your problem.
// ✅ Simple AND well-structured
async function fetchUserOrders(userId: string): Promise<Order[]> {
const user = await users.findById(userId);
if (!user) throw new NotFoundError('User not found');
const orders = await orders.findByUserId(userId);
return orders.filter(o => o.status !== 'cancelled');
}
Simple code is code that a new team member can understand in minutes, not hours.
YAGNI — You Aren’t Gonna Need It
Don’t implement something until you actually need it. Every feature has a cost: code to write, tests to maintain, bugs to fix, and complexity to manage.
The Premature Abstraction Trap
// ❌ Building for imaginary future requirements
class MessageBroker {
private queues = new Map<string, Queue>();
private deadLetterQueue = new DeadLetterQueue();
private retryPolicy = new ExponentialBackoff();
private circuitBreaker = new CircuitBreaker();
private messageSerializer = new AvroSerializer();
// ... 500 lines of code for a system that currently
// only sends welcome emails
}
// ✅ Build what you need NOW
class EmailService {
async sendWelcome(user: User): Promise<void> {
await this.mailer.send({
to: user.email,
subject: 'Welcome!',
template: 'welcome',
data: { name: user.name },
});
}
}
// When you actually NEED queuing, add it then.
The Cost of YAGNI Violations
Every unnecessary feature:
- Takes time to build (that could be spent on actual needs)
- Adds complexity that slows down future development
- Introduces bugs in code paths nobody uses
- Requires testing and maintenance forever
- Confuses new developers who try to understand why it exists
When to Break YAGNI
There are legitimate exceptions:
- Security: Build it in from the start — it’s nearly impossible to bolt on later
- Core architecture decisions: Database choice, API versioning strategy
- Known contractual requirements: If the client says “we’ll need multi-tenant in Q3”
How They Work Together
These three principles complement each other beautifully:
| Situation | DRY says | KISS says | YAGNI says |
|---|---|---|---|
| Similar code in two places | Wait for the third occurrence | Is the duplication actually simpler? | Do we need the abstraction yet? |
| Complex architecture planned | Will it reduce knowledge duplication? | Is there a simpler approach? | Do we need this complexity now? |
| New feature request | Can we reuse existing code? | What’s the simplest solution? | Is this actually needed? |
The sweet spot is code that is:
- DRY enough that changing a business rule means editing one place
- Simple enough that any developer can understand it quickly
- Lean enough that every line of code earns its place
“Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.” — Antoine de Saint-Exupéry