Engineering Atlas › Low-Level Design

Keep it simple (KISS)

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

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:

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

  1. People hold only a few things in working memory at once, so understanding degrades sharply as concepts per path grow.
  2. Most of a system's cost is in maintenance and operation, done by people with less context than the original author.
  3. Every component is a failure mode and every layer is work for the machine, so cost grows with parts, not with lines.
  4. 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

OptionStrengthsWeaknessesChoose when
Minimal design, grow laterFast to build, easy to read and operateMay need a rewrite of one part when needs changeRequirements are uncertain or volumes are modest
General framework up frontHandles many future casesAccidental complexity now for cases that may not comeSeveral real use cases already exist
Data-driven rules (tables, lists)Changeable without deploys, inspectableLimited expressivenessRules are mostly lookups and ordered conditions
Embedded DSL or rule engineVery expressiveA language to maintain, debug and secureNon-engineers must author complex logic at scale
One process, many modulesFunction calls, one deploy, simple debuggingShared release cadenceMost products until team or scale forces a split

Pitfalls in production

Leadership lens

Check yourself

  1. 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.

  2. 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.

  3. 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.

  4. 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

Free resources

Open in the interactive app: practice, quiz, mock interview →