Skip to content

Instantly share code, notes, and snippets.

@maulvi
Created August 9, 2026 15:07
Show Gist options
  • Select an option

  • Save maulvi/8e5a8329ad20371d4227eb04d77c2439 to your computer and use it in GitHub Desktop.

Select an option

Save maulvi/8e5a8329ad20371d4227eb04d77c2439 to your computer and use it in GitHub Desktop.
KISS
name kiss-software-principle
description Apply the KISS (Keep It Simple, Stupid) principle when writing, designing, or reviewing software. Use before writing a new function/class, choosing between implementation approaches, refactoring, or code review — to pick the simplest solution without sacrificing correctness. Includes compensations for known LLM/agent failure modes (context blindness, complexity bias, fabrication, verbose output, unverified confidence).

KISS Principle — Technical Guide for AI Coding Agents

Core rule: if two candidate solutions produce equally correct output for CURRENT requirements, pick the one with fewer lines, dependencies, and branches. Add complexity only when a concrete requirement forces it.

Why This Skill Talks to You Directly, Model

You have documented, measured failure modes that make KISS harder for you specifically than for a human engineer. Each one needs a concrete compensation, not just awareness:

  • Context-blind beyond your window. You don't see the whole codebase or why past decisions were made. Compensation: search/grep existing code for conventions before introducing a new pattern.
  • Biased toward complex, enterprise-style patterns. Training data overrepresents large mature codebases. Compensation: before reaching for a pattern, run it through the Decision Algorithm below instead of pattern-matching from memory.
  • You fabricate with confidence. You can state APIs, file contents, or schema fields that don't exist, as confidently as true ones, and guess instead of checking. Compensation: never assume a function signature, API shape, or config key — read the actual file/docs first. If you can't verify it, say so instead of guessing.
  • You default to verbose output. Left alone, you tend to generate longer code, more comments, and more scaffolding than the task needs — this is a documented tendency, not just a style choice. Compensation: after writing a solution, actively try to remove lines, comments, and files — treat length as a cost to justify, not a sign of thoroughness.
  • You create unnecessary files/artifacts. A known agentic failure is generating extra files, configs, or boilerplate nobody asked for. Compensation: only create files the task explicitly requires; don't scaffold "just in case" structure.
  • You substitute guesses for missing information. When a detail is missing, you tend to invent a plausible one rather than flagging the gap — this causes silent wrong assumptions. Compensation: if a requirement is ambiguous or a dependency is unclear, state the assumption explicitly or ask, rather than picking one silently.
  • You degrade over long sessions. Extended multi-step tasks increase malformed steps, loops, and drift from the original goal. Compensation: break work into small, independently verifiable steps, and re-check each new step against the original requirement, not just the latest message.
  • You cannot fully bound your own blast radius. Compensation: only touch code the current task requires; flag out-of-scope issues instead of fixing them silently.
  • You sound confident while being wrong, and don't own architectural consequences. Compensation: never declare a task complete without running/tracing it against requirements and edge cases; for system-wide changes, state the tradeoff explicitly and prefer the smaller/reversible option.

Simple vs. Naive vs. Over-engineered

State Description Verdict
Naive Short, but skips validation/edge cases/error handling ❌ Bug waiting to happen
Simple Short, covers all current requirements correctly ✅ Target
Over-engineered Long, handles cases that don't exist yet ❌ Wasted complexity
# ❌ Naive
def get_first_price(items):
    return items[0]['price']

# ❌ Over-engineered: unused flexibility for a feature that never needs it
def get_first_price(items, currency="IDR", cache=None, logger=None):
    ...

# ✅ Simple: correct for current requirements, nothing more
def get_first_price(items):
    if not items:
        return None
    return items[0].get('price')

Never cut correctness to save lines. Cut unused flexibility, not necessary checks.

Decision Algorithm

1. Does each candidate produce CORRECT output for current requirements,
   including realistic edge cases (empty, null, wrong type)?
   -> Discard candidates that fail (naive).
2. Score remaining candidates: lines, branches, new dependencies,
   new concepts a reader must learn.
3. Pick the lowest-scoring candidate UNLESS it fails a CONFIRMED
   non-functional requirement (measured performance, security, scale).
4. If it fails, add ONLY the specific complexity needed to fix that
   failure — don't jump to the most elaborate solution available.

Order of work: build the smallest correct version first, clean it up, optimize only after profiling shows a real bottleneck. Never design a "final, complete" architecture upfront — evolve from a small working version.

KISS vs. YAGNI vs. DRY

  • YAGNI: don't build flexibility for a use case that doesn't exist yet. Generalize only after the same logic is needed in 2-3 real call sites (rule of three).
  • DRY can conflict with KISS: a small duplicated snippet (3-5 lines) is often simpler than a generic helper with flags added just to avoid duplication.
  • Tie-breaker: if removing duplication requires a new parameter/flag/branch, keep the duplication until a third real use case exists.
# Two near-identical blocks: simpler to leave as-is (KISS > DRY here)
def format_user_report(user):
    return f"{user.name} - {user.email}"

def format_admin_report(user):
    return f"{user.name} ({user.role}) - {user.email}"

# ❌ Premature DRY: one flexible function with a flag, harder to read
def format_report(user, is_admin=False):
    return f"{user.name} ({user.role}) - {user.email}" if is_admin else f"{user.name} - {user.email}"

Over-Engineering Checklist

A function/change is over-engineered if any of these are true:

  • Its purpose needs "and" to describe (more than one responsibility).
  • More than 2 nested conditional levels.
  • Unused parameter, flag, or branch that no caller exercises.
  • A design pattern (Factory/Strategy/Observer) or generic plugin/parser framework for a single concrete use case.
  • It creates files, configs, or scaffolding not explicitly required by the task.
  • Explaining it takes more than 2-3 sentences.

Error Handling — Not a Place to Cut Corners

KISS applies to design, not to safety. Do not remove validation, error handling, or tests in the name of simplicity — that's where fabrication and unverified confidence do the most damage.

# ❌ Naive: hides real bugs
def load_config(path):
    try:
        return json.load(open(path))
    except:
        return {}

# ✅ Simple: specific, honest, fails loudly
def load_config(path):
    if not os.path.exists(path):
        raise FileNotFoundError(f"Config not found: {path}")
    with open(path) as f:
        return json.load(f)

Catch only what you can meaningfully recover from; let everything else fail with a clear message.

API Design

# ❌ Complex: dict hides what the function needs
def create_user(data):
    return User(name=data['name'], email=data['email'], role=data.get('role', 'user'))

# ✅ Simple: signature is explicit
def create_user(name, email, role="user"):
    return User(name=name, email=email, role=role)

Multi-Language Examples

# Bash — guard clause instead of nested ifs
check_service() {
  [ -f "$1" ] || { echo "File not found"; return 1; }
  [ -x "$1" ] || { echo "Not executable"; return 1; }
  echo "OK"
}
/* C — minimal and correct, no unused generality */
int get_byte(unsigned char *buf, int len, int i) {
    if (i < 0 || i >= len) return -1;
    return buf[i];
}

Decision Table

Situation Lean toward Why
Built-in vs. hand-written Built-in Battle-tested, fewer bugs
One variant vs. design pattern Direct logic Patterns pay off at ≥2-3 real variants
Nested if/else vs. guard clause Guard clause Fewer indentation levels
Large multi-purpose class vs. small functions Small functions Easier to test/reuse
"Just in case" flag vs. hardcode Hardcode now Add flexibility when actually needed
Small duplication vs. generic helper with flags Duplication (until 3rd case) Avoids premature DRY
Complex regex vs. simple string checks String checks (if sufficient) Regex hides bugs
Custom exception hierarchy vs. built-in + message Built-in + clear message Same debuggability, less code
New dependency vs. native solution Native solution Smaller maintenance surface
Generic plugin/parser framework vs. direct script Direct script Build generic only at 2-3 real formats
Guessing an API/schema vs. reading the actual source Read the source Prevents fabrication-driven bugs

Non-Negotiable Rules

  • ALWAYS verify a "simple" solution handles realistic edge cases — simple ≠ naive.
  • ALWAYS use guard clauses instead of deeply nested conditionals.
  • ALWAYS prefer built-in/standard-library functions unless proven otherwise needed.
  • ALWAYS make function signatures explicit rather than passing opaque dicts.
  • ALWAYS build small and working first, evolve later — never design the complete architecture upfront.
  • ALWAYS search the existing codebase for conventions/patterns before introducing a new one.
  • ALWAYS read the actual file/API/schema before relying on it — never assume its shape from memory.
  • ALWAYS run or trace through your code against the stated requirement and edge cases before declaring the task done.
  • ALWAYS limit changes to what the current task requires; flag out-of-scope issues instead of fixing them silently.
  • ALWAYS state assumptions explicitly when a requirement is ambiguous, instead of silently picking one.
  • ALWAYS break multi-step tasks into small, independently verifiable steps.
  • NEVER introduce a pattern or framework for a single concrete use case.
  • NEVER exceed 2 nested conditional levels.
  • NEVER add a parameter/flag/abstraction with zero current callers.
  • NEVER silently swallow errors (bare except:).
  • NEVER remove validation, tests, or error handling to make a diff look simpler.
  • NEVER create a file, config, or module the task didn't ask for.

When Added Complexity Is Justified

Only when proven, not assumed:

  • A real bug/bottleneck exists that only a more complex structure fixes.
  • ≥2-3 real use cases need the same flexibility.
  • A confirmed security/sensitive-data requirement demands it.
  • Measured scale shows the simple version doesn't hold up.

Pre-Commit Checklist

  • Purpose statable in one sentence without "and"?
  • Any unused parameters, branches, or lines?
  • Can I remove one layer without breaking current functionality?
  • Checked for a built-in option before writing this manually?
  • Edge cases handled, not just the happy path?
  • Errors raised with clear messages, not silently caught?
  • Signature explicit about what it actually needs?
  • Did I check existing code/conventions before adding something new?
  • Did I verify the actual API/schema instead of assuming it?
  • Did I actually run/trace this, not just assume it works?
  • Did I stay inside the task's scope, with no extra files created?
  • Could this be shorter without losing correctness?

Rationalizations to Reject

  • "Added for a possible future case" → reject; add when the requirement actually arrives.
  • "Industry-standard pattern, so use it" → reject if current use case doesn't need its flexibility.
  • "Making it generic now for reuse" → reject with fewer than 2-3 real callers.
  • "Catching all exceptions is safer" → reject; it hides bugs instead of preventing them.
  • "Design the complete architecture first" → reject; evolve from a small working version instead.
  • "It compiles/looks right, so it's done" → reject; looking correct and being verified correct are not the same thing for you.
  • "While I'm here, I'll also clean up this other file" → reject unless it's in scope; flag it instead.
  • "This API probably works like the common ones I've seen" → reject; read the actual source before relying on it.
  • "I'll assume this missing detail is X" → reject silently guessing; state the assumption or ask.
  • "More comments/files make it look more thorough" → reject; length is a cost, not a virtue.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment