All Posts

Understanding KISS, YAGNI, and DRY: Key Software Development Principles

Good code isn't just about syntax; it's about philosophy. We explain KISS (Keep It Simple), YAGNI...

Abstract AlgorithmsAbstract Algorithms
ยทยท5 min read
Share
Share on X / Twitter
Share on LinkedIn
Copy link

TLDR: KISS (Keep It Simple), YAGNI (You Aren't Gonna Need It), and DRY (Don't Repeat Yourself) are the three most universally applicable software engineering mantras. They share a common enemy: unnecessary complexity.


๐Ÿ“– The Complexity Tax

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.


๐Ÿ”ข KISS โ€” The Clever Trap

KISS: Keep It Simple, Stupid.

Clever code is not a virtue. 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:

# "Clever" one-liner: hard to read, hard to debug
result = sum(x**2 for x in nums if x > 0) or default

KISS-compliant version:

# 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

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.

KISS tests:

  • Can a new team member understand this in 30 seconds?
  • If this line throws an exception, can you find the bug without a debugger?
  • Could you write this the same way in 6 months?

โš™๏ธ 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:

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")

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:

class PaymentProcessor:
    def __init__(self):
        self.client = StripeClient()  # The only provider we have

When PayPal is actually needed, refactor then โ€” with real requirements, real tests, and real knowledge of what the interface needs to support.

Exceptions to YAGNI:

  • Security: baking in auth hooks now costs little and saves a painful retrofit later.
  • Public APIs: breaking changes to external client interfaces are very costly โ€” design extension points deliberately.

๐Ÿง  DRY โ€” The Copy-Paste Debt

DRY: Don't Repeat Yourself.

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:

# 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")

When security rules change (add special character requirement), both copies must be updated โ€” but only one will be.

DRY fix:

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)

โš–๏ธ When DRY Becomes WET (Wrong Extraction Too)

DRY is about logic, not text. Two functions that look similar but have different reasons to change should stay separate.

# These look duplicated but should NOT be merged
def validate_user_registration_email(email: str) -> None:
    # strict: must be a real domain, must not be disposable, must not exist in DB
    ...

def validate_password_reset_email(email: str) -> None:
    # lenient: just check it's valid format; we don't care if it's in DB
    ...

Merging these with a mode parameter creates an SRP violation โ€” one function with two reasons to change.

Rule: DRY applies to logic that changes for the same reasons. If two pieces of code look similar but diverge when requirements change, they aren't the same knowledge.


๐Ÿ“Œ Summary

  • KISS: Prefer the boring, obvious solution. Clever code is a future liability.
  • YAGNI: Don't build for hypothetical needs. Build when you have real requirements and real tests.
  • DRY: Centralize logic that changes for the same reasons. Every piece of knowledge should have one home.
  • Over-DRY warning: Don't merge code that merely looks similar โ€” if it changes for different reasons, duplication is the right call.

๐Ÿ“ Practice Quiz

  1. You are building a feature that uses Stripe. A teammate wants to add a PayPal integration "just in case." Which principle says no?

    • A) KISS โ€” the code would be too complex.
    • B) YAGNI โ€” build what you need; add PayPal only when there is a concrete requirement to ship it.
    • C) DRY โ€” there would be duplicated payment logic.
      Answer: B
  2. The same email validation regex is copy-pasted into 6 files. The security team adds a new rule. What is the minimum number of files you MUST change to be correct?

    • A) 1 โ€” change it once and update tests.
    • B) 6 โ€” every copy must be updated, which is exactly the DRY violation risk.
    • C) Only the ones that have failed in production so far.
      Answer: B
  3. Two functions both check if user.role == 'admin'. One is for UI access control; one is for data deletion permission. Should they be merged into one function per DRY?

    • A) Yes โ€” they are identical logic and should be one function.
    • B) No โ€” they may diverge (e.g., data deletion may require additional checks later); they represent separate knowledge despite looking similar today.
    • C) Only merge them if they are in the same file.
      Answer: B

Abstract Algorithms

Written by

Abstract Algorithms

@abstractalgorithms