Clean Code

KISS Principle: Keep It Simple, Stupid

I once wrote a configuration system with plugin loading, hot reloading, schema validation, environment-specific overrides, and remote config fetching. The...

16 Apr 2024

KISS Principle: Keep It Simple, Stupid

I once wrote a configuration system with plugin loading, hot reloading, schema validation, environment-specific overrides, and remote config fetching. The project needed a single JSON file read at startup. Nothing more.

That's the opposite of KISS. I built for problems that didn't exist.

What KISS means

KISS — Keep It Simple, Stupid — says prefer the simplest solution that solves the actual problem. Not the most elegant. Not the most extensible. The simplest.

Complexity is a cost. Every abstraction, every layer, every configuration option is something someone has to understand, maintain, and debug. If it doesn't earn its weight, remove it.

Why simplicity wins

Readability. Simple code can be understood by anyone on the team. Complex code can only be understood by the person who wrote it — and even they'll forget in six months.

Fewer bugs. Less code means fewer places for bugs to hide. A 20-line function has a fraction of the bug surface of a 200-line function.

Faster debugging. When something breaks in simple code, the cause is usually obvious. Complex code turns every bug into a detective story.

Easier onboarding. New team members ramp up faster on a simple codebase. Complex architectures require tribal knowledge.

Faster development. Simple solutions ship faster. You spend time solving the problem instead of fighting your own abstractions.

Example: Fibonacci

Here's a version that handles edge cases with explicit branches:

Typescript
function calculateFibonacci(n: number): number[] {
  if (n <= 0) return [];
  if (n === 1) return [0];
  if (n === 2) return [0, 1];

  const sequence: number[] = [0, 1];
  while (sequence.length < n) {
    const next = sequence[sequence.length - 1] + sequence[sequence.length - 2];
    sequence.push(next);
  }
  return sequence;
}

Here's the simpler version:

Typescript
function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

The second is cleaner and easier to understand. It also has exponential time complexity, which is terrible for large n. Sometimes the "complex" version is the right one because it handles real constraints.

How to practice KISS

Start with the dumbest thing that works. You can always add complexity later. You can rarely remove it.

Ask "do I need this?" Before adding a layer, an abstraction, or a configuration option, ask whether the current problem actually requires it. If the answer is "not yet," don't build it.

Resist premature optimization. Optimizing code that runs once per request and takes 2ms is wasted effort. Measure first. Optimize only what matters.

Refactor toward simplicity. When reviewing old code, look for things that can be removed. Dead code, unused parameters, abstractions with only one implementation — cut them.

The trade-off

Simplicity now can mean rework later. If you know a requirement is coming, building for it upfront might save effort. The judgment call is: how confident are you that the requirement will actually arrive?

In my experience, most anticipated requirements don't materialize. The ones that do look different from what you expected. Simple code is easier to extend in the direction reality takes you, not the direction you imagined.

Default to simple. Earn complexity.