Keep it simple (KISS)
Open in the interactive app: practice, quiz, mock interview →In one minute
- KISS ("Keep It Simple, Stupid") says a design should have no more moving parts than the problem requires. Every extra layer, option or service is something to read, test, operate and debug.
- Simple is not the same as easy. Simple means few things tangled together; easy means familiar or close at hand. A framework can be easy to start with and leave a design far from simple.
- Complexity is either essential (the problem really has those rules) or accidental (we added it). KISS targets the accidental part; you cannot simplify away a tax code.
- Complexity costs at runtime too: indirect calls the compiler cannot inline, reflection at startup, larger binaries and dependency trees, and network hops where a function call would do.
- Agents produce plausible complexity cheaply: a factory, a strategy hierarchy and a config system for a feature with one case. The review question is "what is the smallest design that meets today's requirement and does not block tomorrow's?"
How it works
Where the phrase comes from
The phrase is commonly attributed to Kelly Johnson, the engineer who led Lockheed's Skunk Works, and to the idea that an aircraft should be repairable in the field by an average mechanic with basic tools. The software lesson is the same: the person who matters is the one fixing the system at 3 a.m. with partial knowledge, not the one who designed it with full context.
Simple versus easy
Rich Hickey's distinction is the most useful lens. Simple is about structure: one role, one concept, not interleaved with others. Easy is about familiarity: a library you already know, a generator that writes the code for you. Easy things can produce complex results. An ORM that lazily loads relations is easy to use, and it tangles data access with object navigation, so a loop over 200 orders can quietly issue 201 queries. A plain SQL query with a join is less easy and more simple: what it does is visible where it is written.
Essential and accidental complexity
Fred Brooks split complexity into essential (inherent in the problem) and accidental (introduced by our tools and choices). Payroll rules, timezone handling and currency rounding are essential; you can organise them well but not delete them. A message bus between two functions in the same process, a plugin system with one plugin, or a configuration language for three settings are accidental. KISS is the discipline of noticing which kind you are adding.
What a simple design looks like
Consider feature flags. A team designs a rule engine: a DSL parsed into an AST, a plugin registry for operators, and a caching layer. The actual requirement is: enable a flag for internal users, exclude one country, and roll out to a percentage of everyone else. A list of rules evaluated in order, first match wins, does that:
def evaluate(name, flag, user):
for rule in flag["rules"]:
if matches(rule, name, user):
return rule["value"]
return flag["default"]The list is data, so it can live in a JSON file or a database row. It is easy to explain to a product manager, to show in a UI and to test. If the requirements later need nested conditions, the list can grow into a tree; that change is cheap because nothing else depends on an engine that was never built. The practice task builds this evaluator.
How to measure "simple"
There is no single metric, but these work in review:
- Concepts a reader must hold to understand one request path. Count files opened and types traced.
- Moving parts that can fail independently: processes, queues, caches, background jobs.
- Branches per function: cyclomatic complexity above about 10 to 15 is a common review threshold.
- Indirection depth: how many calls from the handler to the line that does the work.
KISS and its neighbours
KISS pulls in the same direction as YAGNI ("you aren't gonna need it"), which says not to build for speculative requirements. It sometimes pulls against DRY: removing duplication can introduce an abstraction harder to read than the two copies. Kent Beck's design rules give a useful priority order: passes the tests, reveals intention, has no duplication, has the fewest elements. Clarity comes before deduplication; fewest elements comes last but still counts.
Reviewing agent-written code
Asked for a payment retry, an agent may produce a RetryPolicyFactory, three strategy classes, an abstract base and a YAML loader, all for one retry rule. Each piece is reasonable in isolation, and the whole is 400 lines where 30 would do. The fix is to ask for the minimal version first and add structure when a second real case appears. Agents also add defensive layers (a cache "for performance", a retry around an idempotency-unsafe call) without measuring; ask what problem each layer solves and what evidence shows it exists.
The same shape elsewhere
KISS is the partial failure shape seen from the design side. Each independent component is another thing that can be slow, down or inconsistent, and failures multiply: five components at 99.9 percent availability each, all on the request path, give about 99.5 percent. The same reasoning drives "choose boring technology" in infrastructure, and checklists over improvisation in aviation and surgery: fewer novel parts, fewer novel failures.
Down the stack
Layer 1The contract
KISS makes no promise to the machine; its contract is with the reader and the operator. A testable form: a new engineer can trace a request from entry point to storage by reading a bounded number of files, every runtime component appears in one architecture diagram, and every configuration option is exercised by a test. When any of these fails, complexity has outgrown what the team can hold.
Layer 2The mechanism
Most accidental complexity enters through mechanisms that do work invisibly. Reflection-based frameworks (DI containers, ORMs, serialisers driven by annotations) inspect types at startup or per call. Dynamic plugin registries resolve implementations by name at runtime, so the compiler cannot check that the plugin exists. Configuration languages become small interpreters with their own bugs. Each third-party dependency brings its transitive tree: a typical npm project resolves hundreds of packages, each a build input, a supply-chain risk and an upgrade task.
On the build side, every module and package is a node in the build graph. More nodes mean more edges, longer incremental rebuilds when a widely imported package changes, and more targets in CI. Splitting code across deploy units is the most expensive form of structure: an interface between two services needs a schema, versioning, retries, timeouts and an on-call owner, where an interface between two packages needs only a function signature.
Layer 3The machine
A direct function call costs about a nanosecond and is often inlined to nothing. An indirect call through an interface or function pointer is similar when predicted, and costs roughly 15 to 20 cycles on a misprediction; more importantly, it blocks inlining and the optimisations that follow. A reflection-based call in Java or Go is commonly tens to hundreds of times slower than a direct call. Each layer also tends to allocate: wrapper objects, boxed values, intermediate collections, which become GC work.
The step change is the network. A call to another process in the same data centre costs on the order of 0.1 to 1 ms once serialisation, the kernel network stack and a TCP round trip are included, which is five to six orders of magnitude slower than an in-process call, and it can fail halfway. Binary size and startup also grow with complexity: a service that pulls in a large framework can take seconds to start, which matters for autoscaling and serverless cold starts. Simple designs are usually fast designs because the machine, like the reader, does less.
Why it's built this way
- People hold only a few things in working memory at once, so understanding degrades sharply as concepts per path grow.
- Most of a system's cost is in maintenance and operation, done by people with less context than the original author.
- Every component is a failure mode and every layer is work for the machine, so cost grows with parts, not with lines.
- So add parts only when a present requirement forces them, and prefer mechanisms (plain calls, data, one process) whose behaviour is visible where they are used.
Diagram
flowchart TD
R["new requirement"] --> Q{"does today's design handle it?"}
Q -- "yes" --> D["change data or config, done"]
Q -- "no" --> E{"essential complexity?"}
E -- "yes" --> S["smallest change that fits it"]
E -- "no, speculative" --> W["wait for a real second case"]
S --> M{"new moving part needed?"}
M -- "no" --> F["function or module in same process"]
M -- "yes" --> J["justify: owner, failure mode, metric"]Worked example
A team builds email and push notifications for an app that sends about 50 messages per second, peaking at 200. The first design uses Kafka, three microservices (intake, templating, delivery), a rules DSL for who gets what, and a Redis cache for templates. It takes four engineers a quarter to build. In the first six months it has nine incidents: two consumer-lag pile-ups, a schema mismatch between intake and templating, a cache returning stale templates after an edit, and several deploy-ordering failures. End-to-end p99 latency is 1.8 seconds, mostly queueing.
A rewrite keeps the essential parts: templates, user preferences, provider retries. It drops the rest. Messages go into a Postgres table with a status column; one worker process selects pending rows with FOR UPDATE SKIP LOCKED, renders the template in-process and calls the provider. Postgres handles a few thousand simple inserts per second on modest hardware, so 200 per second is far below its limits. The rules DSL becomes a list of preference checks in code. The new system is about 1,500 lines instead of 9,000, runs as one deployable, and has had one incident in the following six months (a provider outage, handled by retries). p99 latency fell to 300 ms. If volume ever grows a hundredfold, partitioning the table or adding a queue is a known, bounded change.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Minimal design, grow later | Fast to build, easy to read and operate | May need a rewrite of one part when needs change | Requirements are uncertain or volumes are modest |
| General framework up front | Handles many future cases | Accidental complexity now for cases that may not come | Several real use cases already exist |
| Data-driven rules (tables, lists) | Changeable without deploys, inspectable | Limited expressiveness | Rules are mostly lookups and ordered conditions |
| Embedded DSL or rule engine | Very expressive | A language to maintain, debug and secure | Non-engineers must author complex logic at scale |
| One process, many modules | Function calls, one deploy, simple debugging | Shared release cadence | Most products until team or scale forces a split |
Pitfalls in production
- Abstractions built for a second case that never arrives. Fix: wait for the second real case before generalising, and delete unused extension points.
- Configuration options nobody tests, which fail when finally used. Fix: remove options without a current user, and test every option that remains.
- A cache added "for performance" without measurement, introducing staleness bugs. Fix: measure first; add caching with an explicit invalidation rule and a metric for hit rate.
- Microservices split along technical layers for a small team, turning function calls into RPCs with partial failure. Fix: split only along ownership or scaling boundaries.
- "Simplifying" by deleting essential handling (timezones, retries, idempotency) and moving the complexity onto users or operators. Fix: separate essential from accidental before cutting.
- Clever one-liners that save lines and cost readers minutes. Fix: optimise for the reader; prefer the obvious loop.
Leadership lens
- In design review, ask for the simplest design that meets the current requirement, then ask what would make it insufficient. If the answer is speculative, the simple design wins.
- Make every new moving part (service, queue, cache, framework) justify an owner, a failure mode and a metric. If nobody will be paged for it, it should not exist.
- Count operational cost, not build cost. A design that takes a week longer to simplify but removes an on-call rotation pays back quickly.
- With AI agents writing more code, the cost of producing complexity has dropped and the cost of maintaining it has not. Set norms: smallest working change first, structure added in a separate, justified step.
- Reward deletion. Removing a service, a flag or an abstraction is a real deliverable and should be visible in planning and recognition.
Check yourself
Why can an "easy" tool produce a complex design?
Show answer
Easy means familiar or quick to start; simple means few concerns tangled together. A tool can be quick to adopt and still interleave concerns, such as an ORM mixing data access into object navigation.
What is the difference between essential and accidental complexity, and which does KISS target?
Show answer
Essential complexity comes from the problem itself; accidental complexity comes from tools and design choices. KISS targets the accidental part and organises the essential part well.
Five services on a request path each have 99.9 percent availability. Roughly what is the path's availability, and what does that imply?
Show answer
About 0.999 to the fifth power, roughly 99.5 percent. Each added component multiplies in its failure rate, so extra moving parts have a direct reliability cost.
Why does replacing an in-process call with a network call change more than latency?
Show answer
The call can now fail partially or time out with unknown outcome, so you need retries, idempotency, versioning and monitoring. It also adds serialisation and five to six orders of magnitude more latency.
Watch
- Simple Made Easy (Rich Hickey, Strange Loop 2011): the distinction between simple and easy, and how familiar tools can produce tangled systems.
Free resources
- Simple Made Easy (InfoQ): the same talk with slides, worth rewatching for its vocabulary.
- No Silver Bullet (Fred Brooks, 1986, PDF): the original essential versus accidental complexity argument.
- Yagni (Martin Fowler): the costs of building presumptive features, broken down precisely.
- Beck Design Rules (Martin Fowler): Kent Beck's four rules of simple design and their priority order.
- Choose Boring Technology (Dan McKinley): KISS applied to technology choices, with "innovation tokens" as a budget.
- KISS principle (Wikipedia): origin of the phrase and related principles.