<arch.design/>
Principles/KISS — Keep It Simple
{ }CodeArchitecturebeginner1960generalsimplicityover-engineeringpragmatic

KISS — Keep It Simple

Most systems work best when kept as simple as possible — choose the straightforward solution over the clever one, and add complexity only when the problem demands it.

5/5
{ }
Operates at: Code level

Inside a codebase — classes, modules, files

How it works

KISS ('Keep It Simple, Stupid') originated in the US Navy in the early 1960s as a design principle for systems that field engineers could repair under pressure with minimal tools. Kelly Johnson, the Lockheed Skunk Works engineer, made this a guiding philosophy: design for the real constraints of the people who use it, not for the ideal conditions of the designer.

In software, KISS is a counter to the natural tendency of developers to over-engineer. The clever recursive solution, the generic framework with twelve extension points, the beautifully abstract system designed for hypothetical future requirements — these are KISS violations when a simpler solution would work.

Simplicity has a cost up front (resisting the urge to abstract, suppressing the 'what if' instinct) and a compounding payoff: simple code is easier to read, debug, test, and modify. Complex code is harder to do all of those, and that cost compounds every time someone touches it.

KISS doesn't mean write naive code — it means choose the simplest design that correctly solves the actual problem.

Implementation

TypeScript · Go · Rust
// ❌ Over-engineered — solving a hypothetical problem with a generic framework
abstract class SortStrategy<T> {
  abstract sort(data: T[]): T[];
}
class SortStrategyFactory<T> {
  create(algo: "bubble" | "quick" | "merge"): SortStrategy<T> { /* ... */ }
}
class SortingPipeline<T> {
  constructor(private factory: SortStrategyFactory<T>) {}
  sort(data: T[]): T[] {
    const strategy = this.factory.create("quick");
    return strategy.sort(data);
  }
}
// 30 lines to sort an array

// ✓ Simple — solve the actual problem
function sortByPrice(items: Item[]): Item[] {
  return [...items].sort((a, b) => a.price - b.price);
}
// 1 line. Add complexity only when the problem actually demands it.

Why it matters

Complexity is the enemy of reliability and maintainability. Every additional abstraction, indirection, and moving part is a place where bugs can hide and a place that must be understood before the code can be changed. Simpler code has fewer of both.

When to use

  • Always start with the simplest solution that works
  • When you notice you're designing for hypothetical future requirements
  • When a code review reveals that understanding the code requires extensive context
  • When debugging a system — if the fix requires adding more complexity, reconsider the design

When NOT to use

  • Don't conflate 'simple' with 'naive' — a correct distributed consensus algorithm is necessarily complex
  • Some domains (cryptography, networking protocols) have inherent complexity that can't be wished away

Trade-offs

+

Simple code is fast to read, understand, modify, and debug

Resisting abstraction requires experience and discipline — it's psychologically hard

+

Fewer moving parts means fewer bugs and easier testing

Oversimplification can mean revisiting the design when real complexity arrives

+

New developers onboard faster into simple codebases

Simple today might not handle scale tomorrow — evaluate the actual growth trajectory

In production

SQLite

Single file, no server, no configuration — the simplest possible relational database, used in billions of devices

Go language design

Go deliberately omits generics (originally), inheritance, and many OOP features — simplicity over expressiveness

Unix tools

cat, grep, sort — each does one simple thing; complexity comes from composing them, not from each tool

Industry adoption

5/5Ubiquitous — used at virtually every scale-focused company.

Related principles