Understanding KISS, YAGNI, and DRY: Key Software Development Principles
The Complexity Tax
Every line of code you write is a liability.
π― System Scenario & Requirements
System Goal
Every line of code you write is a liability.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- Every line of code you write is a liability.
- Someone has to read it, test it, debug it, migrate it, document it.
- Code you don't write costs zero.
Constraints & Assumptions
- DRY is about logic, not text .
- Two functions that look similar but have different reasons to change should stay separate.
β‘ Execution Stages & Decision Trace
Stage 1 β KISS β The Clever Trap
KISS: Keep It Simple, Stupid .
Clever code means future-you (or a teammate at 2am during an incident) has to spend 15 minutes understanding what you meant.
Deep Dive
Scenario context
Every line of code you write is a liability. Someone has to read it, test it, debug it, migrate it, document it. Code you don't write costs zero.
KISS, YAGNI, and DRY are three angles of attack on the same problem: code that does more than it needs to, more times than necessary, in harder ways than required.
This walkthrough focuses on The Complexity Tax.
Before making a design choice, clarify:
- Every line of code you write is a liability.
- Someone has to read it, test it, debug it, migrate it, document it.
- Code you don't write costs zero.
Concepts in play: design-principles-solid, jvm-internals-and-garbage-collection, tree-data-structures
Stage 1 β KISS β The Clever Trap
- KISS: Keep It Simple, Stupid .
- Clever code means future-you (or a teammate at 2am during an incident) has to spend 15 minutes understanding what you meant.
- Violation β the unnecessarily clever solution: KISS-compliant version: The second version is slightly longer but O(1) to understand.
- The one-liner has a subtle or default operator that behaves differently from if not squared for edge cases.
# "Clever" one-liner: hard to read, hard to debug
result = sum(x**2 for x in nums if x > 0) or default
# Clear and debuggable
positives = [x for x in nums if x > 0]
squared = [x**2 for x in positives]
result = sum(squared) if positives else default
Observable outcome: The one-liner has a subtle or default operator that behaves differently from if not squared for edge cases.
Stage 2 β YAGNI β The Future-Proofing Fallacy
- YAGNI: You Aren't Gonna Need It (from Extreme Programming).
- Building for hypothetical future requirements is a tax on the present that usually never gets repaid.
- Classic violation: For a startup on Stripe only, this code is three times more complex than necessary , supports two untested paths, and will need to change when real requirements arrive anyway.
- YAGNI-compliant version: When PayPal is actually needed, refactor then β with real requirements, real tests, and real knowledge of what the interface needs to support.
class PaymentProcessor:
def __init__(self, provider: str = "stripe"):
# "We might need PayPal someday" β not asked for
if provider == "stripe":
self.client = StripeClient()
elif provider == "paypal":
self.client = PayPalClient()
elif provider == "adyen":
self.client = AdyenClient() # Never implemented
else:
raise ValueError("Unknown provider")
class PaymentProcessor:
def __init__(self):
self.client = StripeClient() # The only provider we have
Observable outcome: YAGNI-compliant version: When PayPal is actually needed, refactor then β with real requirements, real tests, and real knowledge of what the interface needs to support.
Stage 3 β Deep Dive: DRY β The Copy-Paste Debt
- Every piece of knowledge should have a single, authoritative representation .
- When you copy-paste logic, you create a maintenance bomb: a future change must be made in every copy, and they will diverge.
- Violation: When security rules change (add special character requirement), both copies must be updated β but only one will be.
- DRY fix: π DRY Refactoring: Centralising the Password Validation Rule The diagram contrasts the before and after states of a DRY refactoring.
# File 1: user registration
if len(password) < 8 or not any(c.isdigit() for c in password):
raise ValidationError("Password too weak")
# File 2: password reset
if len(password) < 8 or not any(c.isdigit() for c in password):
raise ValidationError("Password too weak")
def validate_password(password: str) -> None:
if len(password) < 8:
raise ValidationError("Password must be at least 8 characters")
if not any(c.isdigit() for c in password):
raise ValidationError("Password must contain a digit")
# Add future rules here β one place
# Both registration and reset call the same function
validate_password(new_password)
Observable outcome: DRY fix: π DRY Refactoring: Centralising the Password Validation Rule The diagram contrasts the before and after states of a DRY refactoring.
Stage 4 β Principle Decision Flow
- When you are about to write code, run it through this decision flowchart to check which principle applies.
- Reading the flowchart: YAGNI is the first gate β don't build what you don't need.
- DRY is the second β if similar logic exists, check if it's truly the same knowledge before extracting.
- KISS is the third β always prefer the boring, obvious solution.
flowchart TD
A[About to write code] --> B{Is it solving a real current requirement?}
B -- NO --> C[YAGNI: Don't write it]
B -- YES --> D{Does similar logic already exist?}
D -- YES --> E{Does it change for same reason?}
E -- YES --> F[DRY: Extract and reuse]
E -- NO --> G[Keep separate not the same knowledge]
D -- NO --> H{Is there a simpler way to write this?}
H -- YES --> I[KISS: Use the simpler approach]
H -- NO --> J[Write it you're good]
Observable outcome: KISS is the third β always prefer the boring, obvious solution.
Stage 5 β Real-World Application: KISS, YAGNI, and DRY at Scale
- These principles scale from one-liner decisions to architectural choices.
- KISS in production incidents: Post-mortems frequently reveal that "clever" optimizations caused outages.
- A readable, boring approach to rate limiting (a Redis counter with TTL) beats a custom leaky-bucket implementation most engineers won't understand under 2am incident pressure.
- YAGNI in startups: The most common startup engineering failure is over-engineering.
Observable outcome: YAGNI in startups: The most common startup engineering failure is over-engineering.
Stage 6 β Worked Example: Order Price Calculation
- This example walks through an OrderCalculator that simultaneously violates all three principles in a realistic e-commerce domain.
- It was chosen because it compresses KISS (unreadable nested ternaries), YAGNI (an unused cryptocurrency payment path), and DRY (duplicated currency-conversion logic) into a single relatable class β making all three probβ¦
- As you read the "before" block, identify each violation by name before looking at the refactored version, then trace how each helper function addresses exactly one principle.
- Starting point β everything wrong: Problems: - KISS: one-liner with chained ternaries β impossible to debug - DRY: currency conversion logic duplicated in both methods - YAGNI: crypto enabled parameter never used; BTC hβ¦
# π Violates KISS (overcomplicated), DRY (logic repeated), and YAGNI (crypto never needed)
class OrderCalculator:
def calculate_total(self, items, currency="USD", crypto_enabled=False,
apply_b2b_discount=False, loyalty_tier=None):
total = sum(
(i["price"] * i["qty"]) * (0.9 if apply_b2b_discount else 1) *
(0.95 if loyalty_tier == "gold" else 0.97 if loyalty_tier == "silver" else 1)
for i in items
) * (0.9 if currency == "EUR" else 1.1 if currency == "BTC" else 1)
return total
def calculate_shipping(self, items, currency="USD"):
# Copy-pasted currency logic from above
base = 5.0 if len(items) <= 3 else 10.0
return base * (0.9 if currency == "EUR" else 1.1 if currency == "BTC" else 1)
# β
KISS: readable and debuggable
# β
DRY: currency conversion is centralized
# β
YAGNI: no crypto, no unused parameters
def _apply_currency_rate(amount: float, currency: str) -> float:
rates = {"USD": 1.0, "EUR": 0.9} # Only currencies we support NOW
return amount * rates.get(currency, 1.0) # Single place to update rates
def _apply_discount(amount: float, apply_b2b: bool, loyalty_tier: str) -> float:
if apply_b2b:
amount *= 0.90
if loyalty_tier == "gold":
amount *= 0.95
elif loyalty_tier == "silver":
amount *= 0.97
return amount
def calculate_total(items: list, currency: str = "USD",
apply_b2b: bool = False, loyalty_tier: str = None) -> float:
subtotal = sum(item["price"] * item["qty"] for item in items)
discounted = _apply_discount(subtotal, apply_b2b, loyalty_tier)
return _apply_currency_rate(discounted, currency)
def calculate_shipping(items: list, currency: str = "USD") -> float:
base_rate = 5.0 if len(items) <= 3 else 10.0
return _apply_currency_rate(base_rate, currency) # DRY: reuses same function
Observable outcome: Starting point β everything wrong: Problems: - KISS: one-liner with chained ternaries β impossible to debug - DRY: currency conversion logic duplicated in both methods - YAGNI: crypto enabled parameter never used; BTC hβ¦
End-to-end outcome
After working through the stages, you should be able to explain:
- The code you write during normal business hours must be debuggable during an incident at 2am by someone who didn't write it.
- YAGNI is hardest to follow in high-excitement phases.
Alternatives considered
Decision Guide
Decision Guide
Failure and degraded modes
Trade-offs & Failure Modes: When DRY Becomes WET
- Detection: DRY is about logic, not text .
- User or operator impact: DRY is about logic, not text .
