KISS — Keep It Simple, Stupid
Introduction
A junior developer is tasked with writing a function to check if a number is even. They return number % 2 == 0. A senior developer, wanting to show off, writes a recursive bit-manipulation solution with memoization. Both produce the correct result — but one is readable by any teammate at a glance, while the other requires twenty minutes of mental gymnastics to verify.
The senior developer's solution is not better. It is worse — because it introduces unnecessary complexity for zero practical benefit.
KISS — Keep It Simple, Stupid — is the principle that software should be as simple as possible, but no simpler. It was coined by Kelly Johnson, the lead engineer at Lockheed's Skunk Works, who famously challenged his engineers to design jet aircraft that could be repaired by an average mechanic in the field using only common tools. If the design required specialized knowledge or equipment, it failed the KISS test.
In software, KISS means choosing the simplest solution that meets the requirements. Not the most elegant. Not the most clever. Not the most extensible. The simplest. This tutorial explores why simplicity is a feature, how to recognize unnecessary complexity, and when KISS conflicts with other principles like DRY (previous tutorial) and the Open/Closed Principle (Section 6).

Why Simplicity Matters
Software spends far more time being read and maintained than being written. Studies consistently show that 70% or more of total software cost is in maintenance — the reading, understanding, debugging, and modifying of existing code (as we discussed in S1).
Every line of code, every abstraction layer, every design pattern adds to the cognitive load required to understand the system. Unnecessary complexity has real costs:
- Slower onboarding — New team members take longer to become productive
- More bugs — Complex code has more places for bugs to hide and more edge cases to handle
- Harder debugging — When something breaks in a four-layer abstraction, finding the root cause takes exponentially longer than in a direct implementation
- Resistance to change — Developers hesitate to modify code they do not fully understand, leading to workarounds and patches instead of proper fixes
Simplicity is not the absence of sophistication. It is the result of deeply understanding a problem and finding the most direct path to solving it. As Tony Hoare said: "There are two ways of constructing a software design: one way is to make it so simple that there are no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies."
KISS Violation — The Over-Engineered Calculator
The requirement is straightforward: calculate the total price of items in a shopping cart with a percentage discount. Here is what over-engineering looks like.
from abc import ABC, abstractmethod
from typing import List, Protocol
# Over-engineered: Strategy pattern, factory, protocol — for a simple discount
class DiscountStrategy(Protocol):
def apply(self, total: float) -> float: ...
class PercentageDiscount:
def __init__(self, percent: float) -> None:
self._percent = percent
def apply(self, total: float) -> float:
return total * (1 - self._percent / 100)
class NoDiscount:
def apply(self, total: float) -> float:
return total
class DiscountFactory:
@staticmethod
def create(discount_type: str, value: float) -> DiscountStrategy:
if discount_type == "percentage":
return PercentageDiscount(value)
return NoDiscount()
class PricingEngine:
def __init__(self, strategy: DiscountStrategy) -> None:
self._strategy = strategy
def calculate(self, prices: List[float]) -> float:
subtotal = sum(prices)
return self._strategy.apply(subtotal)
# Usage — 6 lines to do what 1 line could do
factory = DiscountFactory()
strategy = factory.create("percentage", 10)
engine = PricingEngine(strategy)
result = engine.calculate([29.99, 9.99, 49.99])
print(f"Total: ${result:.2f}")import java.util.List;
// Over-engineered: 4 classes for a simple calculation
interface DiscountStrategy {
double apply(double total);
}
class PercentageDiscount implements DiscountStrategy {
private final double percent;
public PercentageDiscount(double percent) {
this.percent = percent;
}
@Override
public double apply(double total) {
return total * (1 - percent / 100);
}
}
class NoDiscount implements DiscountStrategy {
@Override
public double apply(double total) {
return total;
}
}
class DiscountFactory {
public static DiscountStrategy create(String type, double value) {
if ("percentage".equals(type)) {
return new PercentageDiscount(value);
}
return new NoDiscount();
}
}
class PricingEngine {
private final DiscountStrategy strategy;
public PricingEngine(DiscountStrategy strategy) {
this.strategy = strategy;
}
public double calculate(List<Double> prices) {
double subtotal = prices.stream().mapToDouble(Double::doubleValue).sum();
return strategy.apply(subtotal);
}
}
// Usage
public class Main {
public static void main(String[] args) {
DiscountStrategy strategy = DiscountFactory.create("percentage", 10);
PricingEngine engine = new PricingEngine(strategy);
double result = engine.calculate(List.of(29.99, 9.99, 49.99));
System.out.printf("Total: $%.2f%n", result);
}
}Four classes, a factory, and a protocol — for a calculation that the requirements describe as a simple percentage discount. There is no indication that the discount strategy will ever change or that multiple discount types are needed.
This is what happens when developers apply design patterns prophylactically — anticipating future requirements that may never arrive. The Strategy Pattern is a great tool, but using it where a single function suffices violates KISS. We will cover Strategy in Section 12 and see where it genuinely adds value.
Applying KISS — The Direct Solution
The KISS version solves the same problem with far less code. It is immediately readable, trivially testable, and easy to extend if and when the requirements actually expand.
from typing import List
def calculate_total(prices: List[float], discount_percent: float = 0) -> float:
"""Calculate total price with optional percentage discount."""
subtotal = sum(prices)
return subtotal * (1 - discount_percent / 100)
# Usage — one function, one call, done
total = calculate_total([29.99, 9.99, 49.99], discount_percent=10)
print(f"Total: ${total:.2f}") # Total: $80.97import java.util.List;
public class PriceCalculator {
public static double calculateTotal(List<Double> prices, double discountPercent) {
double subtotal = prices.stream().mapToDouble(Double::doubleValue).sum();
return subtotal * (1 - discountPercent / 100);
}
public static void main(String[] args) {
double total = calculateTotal(List.of(29.99, 9.99, 49.99), 10);
System.out.printf("Total: $%.2f%n", total); // Total: $80.97
}
}One function. One responsibility. Zero unnecessary abstractions. Any developer can understand this in seconds.
If the business later requires multiple discount types (flat discount, buy-one-get-one, tiered pricing), then you refactor toward Strategy. This is the essence of KISS combined with YAGNI (next tutorial): solve today's problem today; solve tomorrow's problem tomorrow.
Note that the KISS solution is also easier to test. The over-engineered version requires instantiating a factory, creating a strategy, injecting it into an engine, and providing prices — four steps to test one calculation. The KISS version requires calling one function with two arguments.
Visualization

Recognizing Unnecessary Complexity
How do you know when code is unnecessarily complex? Here are the warning signs:
1. Abstraction Layers With One Implementation
You create an interface PaymentProcessor, a factory PaymentProcessorFactory, and exactly one concrete class StripePaymentProcessor. The interface and factory add cognitive overhead for zero current benefit. KISS says: use StripePaymentProcessor directly. If you add a second payment processor later, then extract the interface.
2. Indirection Without Purpose
Method A calls method B, which calls method C, which does the actual work. B adds no logic, no validation, no transformation — it is a passthrough. Every layer of indirection is a layer of reading that a future maintainer must navigate. Remove the middleman.
3. Premature Generalization
A function takes twelve parameters because it handles every conceivable variation. In practice, it is called in exactly two ways with specific parameter combinations. Write two focused functions instead.
4. Clever Code
Code that uses advanced language features, bitwise tricks, or nested ternaries to save lines at the cost of readability. If another competent developer needs more than 30 seconds to understand what a function does, it is too clever.
5. Design Patterns as Resume Decorations
Applying Observer, Strategy, Abstract Factory, and Command to a 200-line script. Patterns solve recurring design problems — they are not achievements to collect. If you are adding a pattern without a concrete problem it solves, you are violating KISS.
Common Mistakes
Mistake 1: Confusing Simple with Sloppy
❌ Wrong: "KISS says keep it simple, so I'll put everything in one 500-line function with no structure."
✅ Right: KISS advocates for the simplest good solution — not the laziest one. A well-structured function with clear naming, single responsibility, and logical flow is simpler to understand than a wall of code, even if it has more lines. Simplicity serves comprehension, not brevity.
Mistake 2: Refusing to Use Patterns When They Genuinely Help
❌ Wrong: "KISS says no abstractions, so I'll use if-else chains with 15 branches instead of Strategy."
✅ Right: When the problem genuinely has multiple interchangeable behaviors (like payment methods, sorting algorithms, or pricing strategies), a pattern is the KISS solution — the 15-branch if-else is actually more complex. KISS is not anti-pattern; it is anti-premature-pattern.
Mistake 3: Using KISS to Justify Skipping Error Handling
❌ Wrong: "KISS says keep it simple, so I will not handle the case where the database is unavailable."
✅ Right: Error handling for realistic failure scenarios is part of the requirements, not optional complexity. KISS means not over-engineering the solution — it does not mean ignoring the problem.
KISS and Other Principles — Finding the Balance
KISS sometimes conflicts with other design principles. When it does, here is how to think about the tradeoff:
KISS vs DRY (previous tutorial): DRY says extract shared logic. But sometimes the extraction creates a complex shared utility that is harder to understand than the original duplication. When the cost of the abstraction exceeds the cost of the duplication, choose the simpler path — tolerate the duplication.
KISS vs Open/Closed Principle (Section 6): OCP says design for extension without modification. But building extension points for changes that may never come adds complexity today. KISS says solve today's problem. Refactor toward OCP when a second variation actually appears.
KISS vs Separation of Concerns: Splitting a 30-line class into three 10-line classes with interfaces between them can be over-engineering. If the class has a single cohesive purpose and is easy to understand, it is already well-designed.
The guiding question: "Does this complexity address a problem I have right now, or a problem I imagine having in the future?" If it serves a real, current need, the complexity is justified. If it is insurance against hypothetical change, it violates KISS.
Key Takeaways
- KISS means choosing the simplest solution that correctly meets the current requirements — not the cleverest, not the most extensible, not the most pattern-heavy
- Unnecessary complexity has ongoing costs: harder onboarding, more bugs, slower debugging, resistance to change
- Design patterns are tools for solving real problems — applying them without a concrete need is over-engineering, not good design
- Simple code is easier to refactor, not harder — when requirements genuinely change, a direct implementation is faster to evolve than a speculative abstraction
- KISS does not mean sloppy — it means deeply understanding the problem and finding the most direct solution
- When KISS conflicts with DRY or OCP, ask: "Does this complexity solve a problem I have right now?" — if not, keep it simple and refactor when the need is real