| 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). |
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.
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.
| 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.
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.
- 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}"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.
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.
# ❌ 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)# 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];
}| 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 |
- 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.
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.
- 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?
- "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.