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:

  1. Security: Build it in from the start — it’s nearly impossible to bolt on later
  2. Core architecture decisions: Database choice, API versioning strategy
  3. 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:

SituationDRY saysKISS saysYAGNI says
Similar code in two placesWait for the third occurrenceIs the duplication actually simpler?Do we need the abstraction yet?
Complex architecture plannedWill it reduce knowledge duplication?Is there a simpler approach?Do we need this complexity now?
New feature requestCan 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