DRY, KISS, and YAGNI

beginner 0-2 YOE lld clean-code dry kiss yagni

These three principles are so simple they fit on a sticky note, but they’ll save us from writing mountains of unnecessary code. Let’s break each one down.

DRY — Don’t Repeat Yourself

If we find ourselves writing the same logic in two places, that’s a sign to extract it into a shared function or class.

Why? Because when the logic changes (and it will), we only want to change it in ONE place. Copy-pasted code means copy-pasted bugs.

# BAD -- same validation logic in two places
def create_user(email):
    if "@" not in email or "." not in email:
        raise ValueError("Invalid email")
    # create user...

def update_user(email):
    if "@" not in email or "." not in email:
        raise ValueError("Invalid email")
    # update user...

# GOOD -- extract the repeated logic
def validate_email(email):
    if "@" not in email or "." not in email:
        raise ValueError("Invalid email")

def create_user(email):
    validate_email(email)
    # create user...

def update_user(email):
    validate_email(email)
    # update user...
// BAD -- same validation in two places
function createUser(email) {
  if (!email.includes("@") || !email.includes(".")) {
    throw new Error("Invalid email");
  }
  // create user...
}

function updateUser(email) {
  if (!email.includes("@") || !email.includes(".")) {
    throw new Error("Invalid email");
  }
  // update user...
}

// GOOD -- extract the repeated logic
function validateEmail(email) {
  if (!email.includes("@") || !email.includes(".")) {
    throw new Error("Invalid email");
  }
}

function createUser(email) {
  validateEmail(email);
  // create user...
}

function updateUser(email) {
  validateEmail(email);
  // update user...
}
// BAD -- repeated validation
void createUser(String email) {
    if (!email.contains("@") || !email.contains("."))
        throw new IllegalArgumentException("Invalid email");
    // create user...
}

void updateUser(String email) {
    if (!email.contains("@") || !email.contains("."))
        throw new IllegalArgumentException("Invalid email");
    // update user...
}

// GOOD -- extracted
void validateEmail(String email) {
    if (!email.contains("@") || !email.contains("."))
        throw new IllegalArgumentException("Invalid email");
}

void createUser(String email) {
    validateEmail(email);
    // create user...
}

When DRY Goes Too Far

Here’s the catch — sometimes two pieces of code LOOK the same but serve different purposes. If we force them together, a change in one breaks the other. This is called premature abstraction.

If two functions happen to have similar code but exist for different reasons, it’s okay to let them be. Wait until the duplication proves to be a real problem before abstracting.

KISS — Keep It Simple, Stupid

The simplest solution that works is usually the best. Don’t write clever code when straightforward code will do.

# BAD -- clever but hard to read
def is_even(n):
    return not n & 1

# GOOD -- anyone can understand this
def is_even(n):
    return n % 2 == 0

# BAD -- over-engineered for a simple task
class StringReverserFactory:
    def create_reverser(self):
        return StringReverser()

class StringReverser:
    def reverse(self, s):
        return s[::-1]

# GOOD -- just a function
def reverse_string(s):
    return s[::-1]
// BAD -- clever one-liner nobody wants to debug
const flattenDeep = (a) =>
  a.reduce((acc, v) => acc.concat(Array.isArray(v) ? flattenDeep(v) : v), []);

// GOOD -- readable and clear
function flattenDeep(arr) {
  return arr.flat(Infinity);
}

// BAD -- over-engineered
class ConfigManager {
  #instance;
  static getInstance() {
    /* ... singleton pattern for a simple config ... */
  }
}

// GOOD -- for simple cases, just use an object
const config = { port: 3000, host: "localhost" };
// BAD -- over-engineered for simple string joining
class StringJoinerBuilder {
    private StringBuilder sb = new StringBuilder();
    StringJoinerBuilder add(String s) { sb.append(s); return this; }
    String build() { return sb.toString(); }
}

// GOOD -- use what the language gives us
String result = String.join(", ", "a", "b", "c");

KISS doesn’t mean “write the fewest characters.” It means “write code that’s easy to read, understand, and modify.” Future us (and our teammates) will thank us.

YAGNI — You Ain’t Gonna Need It

Don’t build features we think we MIGHT need later. Build what we need NOW.

This one is hard because as developers, we love to plan ahead. “What if we need to support multiple currencies?” “What if we need to handle 10 million users?” But until those requirements actually exist, we’re just adding complexity for nothing.

// BAD -- building for imaginary requirements
"Let's add a plugin system for our TODO app"
"Let's make this support 15 databases just in case"
"Let's add multi-language support for our internal tool"

// GOOD -- build what's needed
"We need to add and complete TODOs. Let's do that."
"We use PostgreSQL. Let's support PostgreSQL."
"Our team speaks English. Let's write it in English."

The cost of building something we don’t need:

  • Time wasted writing and testing unused code
  • Complexity added that makes real features harder to build
  • Bugs introduced in code that nobody asked for
  • Maintenance burden for features nobody uses

How They Work Together

PrincipleMantraAnti-Pattern
DRY”One source of truth”Copy-pasting the same logic
KISS”Simple beats clever”Over-engineering a simple problem
YAGNI”Build it when we need it”Building features speculatively

In LLD interviews, these principles guide our design instincts:

  • DRY: Extract shared logic into base classes or utility methods
  • KISS: Don’t apply every design pattern we know — use the simplest solution
  • YAGNI: Don’t add interfaces and abstractions until there’s a real reason for them

A Parking Lot system doesn’t need a PluginManager. It needs to park cars. Start simple, extend when needed.