← Back to principles

Design Principle

DRY, KISS & YAGNI

Balance reuse, simplicity, and restraint to keep systems adaptable.

Three short acronyms, one shared goal: helping you decide when to extract, when to simplify, and when to wait. Treat them as levers, not laws.


DRY — Don’t Repeat Yourself

DRY is about deduplicating knowledge. Shared functionality should live in one place only if updates belong together.

Extract shared business rulesTS
1// Two endpoints validate discount codes differently.
2function applyCheckoutDiscount(code: string) {
3if (code.length !== 10) throw new Error("Invalid");
4if (!code.startsWith("PROMO-")) throw new Error("Unknown");
5}
6
7function applyRenewalDiscount(code: string) {
8if (code.length !== 10) throw new Error("Invalid");
9if (!code.startsWith("PROMO-")) throw new Error("Unknown");
10}
11
12// Extract the shared knowledge; pass in contextual behaviour.
13const validateDiscount = (code: string) => {
14if (code.length !== 10) throw new Error("Invalid");
15if (!code.startsWith("PROMO-")) throw new Error("Unknown");
16};
17
18function applyCheckoutDiscount(code: string) {
19validateDiscount(code);
20// ...
21}
22
23function applyRenewalDiscount(code: string) {
24validateDiscount(code);
25// ...
26}

Marker: If a bug fix needs to land twice, reuse the knowledge once.


KISS — Keep It Simple, Ship It

KISS turns clever abstractions into clear flows. Optimise for readability over novelty; measure complexity in “how long to understand”.

Prefer simple data over smart objectsTS
1// Clever: chained factories and context-aware constructors.
2const user = User.create()
3.withProfile(profileFactory("author"))
4.withNotifications(enabledNotifications())
5.build();
6
7// Simple: shape data clearly, let functions consume it.
8const user = {
9profile: authorProfile(),
10notifications: enabledNotifications(),
11};
12
13registerUser(user);

Marker: When onboarding teammates, note where explanations feel apologetic. That’s your KISS debt.


YAGNI — You Aren’t Gonna Need It

YAGNI keeps speculative architecture at bay. Delay features until a real user or system pressure demands them.

Defer until pressure arrivesTS
1// Premature generalisation: multi-provider plumbing without users.
2abstract class StorageProvider {
3abstract put(key: string, value: string): Promise<void>;
4}
5
6class S3Storage extends StorageProvider {/* ... */}
7class GCSStorage extends StorageProvider {/* ... */}
8
9// Practical: solve today's need, keep the seam obvious.
10type Storage = {
11put(key: string, value: string): Promise<void>;
12};
13
14const storage: Storage = {
15async put(key, value) {
16 await s3Client.putObject({ key, value });
17},
18};

Marker: If a counterfactual architecture has no customer, defer it. Document assumptions and revisit when evidence appears.


How They Interact

  • DRY vs. KISS: Don’t extract at the cost of clarity. If a helper function hides context, duplication might be kinder.
  • KISS vs. YAGNI: A simple solution is often the one you can delete later. Resist introducing indirection until the system insists.
  • DRY vs. YAGNI: Extract only after repetition proves intent. Otherwise you create abstractions nobody needs yet.

Reflection

  1. Track the last three bugs. Were they caused by duplication, complexity, or premature abstraction?
  2. Pair-program a refactor: remove one abstraction and measure whether reviews speed up.
  3. Establish a principle check in design docs: “What evidence says we need this now?”

Takeaway: DRY, KISS, and YAGNI keep you honest about trade-offs: share knowledge carefully, design for comprehension, and defer the future until it knocks.


Interview Questions

Beginner

Q: What does DRY stand for and why is it important? Give an example of code that violates DRY.

A: DRY stands for "Don't Repeat Yourself" - it means eliminating duplication of knowledge or logic. If the same business rule or validation appears in multiple places, extract it to a single source.

Example violation: Two functions validate discount codes with identical logic:

function applyCheckoutDiscount(code: string) {
  if (code.length !== 10) throw new Error("Invalid");
  if (!code.startsWith("PROMO-")) throw new Error("Unknown");
}

function applyRenewalDiscount(code: string) {
  if (code.length !== 10) throw new Error("Invalid");
  if (!code.startsWith("PROMO-")) throw new Error("Unknown");
}

Fix: Extract validateDiscount(code) and call it from both functions. If validation rules change, you only update one place.


Intermediate

Q: When should you NOT apply DRY? How do you balance DRY with KISS?

A: Don't apply DRY when:

  1. The duplication is coincidental (same code, different reasons to change)
  2. Extracting would hide important context or make code harder to understand
  3. The abstraction would be more complex than the duplication

Balance with KISS: If extracting shared logic creates a confusing abstraction that requires reading multiple files to understand, the duplication might be kinder. For example, two similar but contextually different validations shouldn't be forced into one generic validator if it obscures their distinct purposes. Measure complexity in "how long to understand" - if the DRY version takes longer to grasp, prefer KISS.


Senior

Q: You're designing a multi-tenant SaaS platform. How do you apply DRY, KISS, and YAGNI when building tenant isolation? What abstractions would you create now vs. defer?

A:

DRY: Extract tenant context resolution into a single middleware/service. Don't repeat tenant ID extraction in every endpoint.

KISS: Start with row-level security (add tenant_id to tables, filter queries). Don't build a complex multi-database architecture until you have evidence you need it.

YAGNI:

  • Defer: Cross-tenant analytics, tenant-specific database schemas, per-tenant infrastructure isolation
  • Build now: Basic tenant ID filtering, tenant context injection, simple data isolation

Abstractions to create now:

  • TenantContext interface for accessing current tenant
  • TenantAwareRepository base class that auto-filters by tenant_id
  • Middleware that extracts tenant from request and injects into context

Abstractions to defer:

  • Multi-database routing layer (wait until you have 100+ tenants and performance issues)
  • Tenant-specific feature flags system (wait until you have actual feature divergence requirements)
  • Cross-tenant data sharing mechanisms (wait until a customer actually requests it)

The key: Build the minimum viable tenant isolation that works, document assumptions, and revisit when evidence demands more complexity.


Key Takeaways

  • DRY eliminates duplication of knowledge - if a bug fix needs to happen twice, extract the shared logic once
  • KISS prioritizes clarity over cleverness - optimize for readability, measure complexity in "how long to understand"
  • YAGNI defers speculative features - delay capabilities until real user or system pressure demands them
  • DRY vs. KISS: Don't extract at the cost of clarity - if a helper hides context, duplication might be kinder
  • KISS vs. YAGNI: Simple solutions are often easier to delete later - resist indirection until the system insists
  • DRY vs. YAGNI: Extract only after repetition proves intent - otherwise you create abstractions nobody needs yet
  • Track bugs: Were they caused by duplication, complexity, or premature abstraction?
  • Establish principle checks in design docs: "What evidence says we need this now?"

Keep exploring

Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.