Filter concepts by levelShowing all levels.

System Design · Section 99

High-Level Design vs Low-Level Design

Level
intermediate
Read
16 min
Concepts
3

A high-level design answers "what are the moving parts and how does data get between them". Its vocabulary is components and services, the data stores each one owns, the queues and streams connecting them, the network and trust boundaries they sit inside, and the two or three flows carrying most of the system's value — with synchronous and asynchronous edges distinguished, because that difference determines failure behaviour. Boundaries deserve the most attention, since a boundary is where properties change: crossing a process boundary introduces failures an in-process call cannot have, crossing a trust boundary introduces authentication and authorization, and crossing a network boundary introduces a timeout. Nothing about the inside of a box belongs there, and the test is whether the design would still read correctly if one component's internals were replaced entirely. A low-level design answers "how is one component actually built": modules and their boundaries, classes and interfaces, the schema with its keys, indexes and constraints, the state machines governing how entities change, the algorithms where the choice matters, and the internal APIs between modules. The schema and the state machine carry the most weight — the first outlives the code and binds every future writer, the second turns unenumerated cases into visible gaps — and the test is implementability: a reader who knows the codebase should not have to ask a design question. The actual skill is the movement between them. Design conversations slide downward on their own, because concrete details are easier to discuss, and the result is a mixed document too vague to evaluate as an architecture and too incomplete to build from. So descend deliberately and say you are doing it, keep one level per artefact, and run both consistency checks: every high-level component has a low-level design or an explicit note that it does not need one, and every store, queue and external call appearing in a low-level design also appears on the high-level diagram — because a missing one is almost always a dependency nobody reviewed.

What is true here

  1. High level: components, owned stores, queues, boundaries and two or three major flows — replace any component's internals and it should still read correctly.
  2. Low level: one component's modules, schema, state machines, algorithms and internal APIs — implementable without asking a design question.
  3. Boundaries carry the most weight at the high level; the schema and state machine carry it at the low level.
  4. Mixed-level documents fail at both jobs, and the mixing happens by drift rather than by decision.
  5. Run both consistency checks — a store present at the low level and absent at the high level is an unreviewed dependency.

What you will be able to do

  • Sort a list of design facts into the level each one belongs at, and justify each placement with the level's own test
  • Mark network, process and trust boundaries on a high-level design and say what changes at each
  • Write a component's state machine and schema to the point where implementation raises no design questions
  • Descend into a component and carry the architectural findings back up to the high-level design

What each level holds

The vocabulary of a high-level design and of a low-level design, each with the test that marks it correct.

What belongs in a high-level design

standardintermediate

A high-level design answers "what are the moving parts, and how does data get between them". Its vocabulary is components and services, the data stores each one owns, the queues and streams that connect them asynchronously, the network and trust boundaries they sit inside, and the major flows — the two or three paths that carry most of the system's traffic and value. The boundaries deserve particular attention, because a boundary is where the properties change: a call that crosses a process boundary can fail in ways an in-process call cannot, a call that crosses a trust boundary needs authentication and authorization, and a call that crosses a network boundary needs a timeout. Marking them is what makes the diagram say something rather than merely arrange things. What does not belong is anything about the inside of a box: class names, method signatures, schema columns, algorithms. Not because those are unimportant, but because a reader at this level is asking a different question, and mixing the two produces a diagram that is too detailed to see the shape and too incomplete to implement from. A good test is whether the design still reads correctly if one component's internals were replaced entirely — if it does, the level is right.

Think of it as

A map of a city's transport network. It shows lines, interchanges, zones and where the river is. It deliberately does not show the seat layout of a carriage, and adding that would make the map worse at the one thing it exists for. A high-level design is that map: the right level of detail is the level at which someone can see how to get from one place to another and where the crossings are.

text
A high-level design, written out

  [browser] --https--> (edge/CDN)
                          |
                    == trust boundary ==
                          v
                     (api gateway) --sync--> (orders svc)
                                                 |  owns
                                                 v
                                             [orders db]
                                                 |
                          (orders svc) --async--> {events}
                                                    |
                                             (search indexer)
                                                    |  owns
                                                    v
                                              [search index]

  Major flows: 1) place an order  2) search orders
The level a high-level design operates at
syncasync

Client

Edge / CDN

trust boundary crossed here

API gateway

Orders service

owns the orders database

Orders database

Event stream

asynchronous edge

Search indexer

owns the search index

  • Client
    • leads to Edge / CDN
  • Edge / CDN — trust boundary crossed here
    • leads to API gateway
  • API gateway
    • leads to Orders service (sync)
  • Orders service — owns the orders database
    • leads to Orders database
    • leads to Event stream (async)
  • Orders database
  • Event stream — asynchronous edge
    • leads to Search indexer
  • Search indexer — owns the search index

In a high-level design, and not

In a high-level design, and not
BelongsDoes not belong
Services and components, with responsibilitiesClass and module names
Data stores, and which component owns eachTable columns, indexes, constraints
Queues and streams, and what flows through themMessage field-level schemas
Network, process and trust boundariesFunction signatures
Two or three major data flowsEvery path through the system
Synchronous versus asynchronous edgesRetry counts and backoff constants

Remember: A high-level design names the components and services, the stores each one owns, the queues between them, the network and trust boundaries, and the two or three major flows — and marks which edges are synchronous. Boundaries matter most, because they are where failure modes and security requirements change. Nothing about the inside of a box belongs here: if a component's internals could be replaced entirely and the design still read correctly, the level is right.

See also: what belongs in a low level design · moving between abstraction levels · the high level design checklist · layered architecture · architecture vs implementation

What belongs in a low-level design

standardintermediate

A low-level design answers "how is one component actually built". Its vocabulary is modules and their boundaries, the classes and interfaces inside them, the database schema with its columns, keys, indexes and constraints, the state machines that govern how an entity changes, the algorithms chosen for the parts where the choice matters, and the internal APIs one module offers another. Its scope is deliberately one component — a low-level design that spans three services has drifted into being a poor high-level design. Two elements carry more weight than the rest. The schema, because it outlives the code around it and because constraints written there are enforced against every future writer, which is the difference between a rule and a hope. And the state machine, because most real-world defects are states nobody enumerated: a listed set of states and permitted transitions turns "what happens if the payment succeeds after the hold expired" from an open question into either a transition on the diagram or a gap you can see. The test for the right level here is implementability — a reader who knows the language and the codebase should be able to write the component from it without asking a design question, and if they cannot, the missing piece belongs in the document.

Think of it as

The wiring diagram for one building on the city map. It names every circuit, every junction and every rating, and it says nothing about the bus routes outside. Somebody has to be able to build from it without asking what the architect meant. If reading it raises a design question rather than an implementation question, it is not finished.

sql
-- schema: the part that outlives the code
CREATE TABLE holds (
  seat_id    uuid PRIMARY KEY,
  buyer_id   uuid        NOT NULL REFERENCES buyers,
  state      text        NOT NULL,
  expires_at timestamptz NOT NULL,
  CHECK (state IN ('held', 'converting', 'released'))
);
CREATE INDEX holds_expiry ON holds (expires_at)
  WHERE state = 'held';   -- partial: only live rows

-- states: held -> converting -> (booked | released)
--         held -> released (expiry or cancel)
A state machine is the highest-value part of a low-level design
paymentstartedexpiry orcancellationpayment succeeded,hold still validpayment failed, orhold expired first

held

start

converting (payment in flight)

booked

end

released

end

  • held (start)
    • → converting (payment in flight) when payment started
    • → released when expiry or cancellation
  • converting (payment in flight)
    • → booked when payment succeeded, hold still valid
    • → released when payment failed, or hold expired first
  • booked (end)
  • released (end)

In a low-level design, and not

In a low-level design, and not
BelongsDoes not belong
Module boundaries and responsibilitiesWhich services exist in the system
Classes, interfaces, and their contractsNetwork topology
Schema: columns, keys, indexes, constraintsWhich component owns which store
State machines and permitted transitionsCross-service flows
Algorithms where the choice mattersDeployment topology
Internal API signatures and error shapesPublic API versioning strategy

Remember: A low-level design covers one component: its modules, classes and interfaces, its schema with keys, indexes and constraints, its state machines, the algorithms where the choice matters, and the internal APIs between modules. The schema and the state machine carry the most weight — one outlives the code and binds every future writer, the other turns unenumerated cases into visible gaps. The test is implementability: a reader who knows the codebase should not have to ask a design question.

See also: what belongs in a high level design · moving between abstraction levels · status and lifecycle modeling · choosing enforcement mechanisms

Advertisement

Moving between them

Descending on purpose, keeping one level per artefact, and running the consistency checks in both directions.

Moving between levels without mixing them

coreintermediate

The skill is not knowing what belongs at each level — it is descending deliberately and announcing the move. A design conversation naturally slides downward, because concrete details are easier to discuss than abstract ones and everyone has an opinion about a column name. The result is a mixed document: three services sketched at cartoon depth alongside one table's exact indexes, which is too vague to evaluate as an architecture and too incomplete to build from. The discipline has three parts. Descend on purpose: finish the level you are on, say "let me go a level down into the fetch path", and come back up when that path is done. Keep one level per artefact, so a high-level diagram never grows a schema and a component design never grows a network topology — if a detail is needed for the higher level to make sense, it is usually a boundary or a data-ownership fact, both of which are legitimately high-level. And carry consistency between levels: every component named at the high level should have a low-level design or an explicit note that it does not need one, and every store or queue in a low-level design should appear on the high-level diagram. That last check is the one that catches real problems, because a store appearing at the low level and not the high level usually means a component quietly took a dependency nobody reviewed.

Think of it as

Zooming a map. At city scale you see districts and roads; at street scale you see house numbers. Both are correct, and a map that renders house numbers on three streets and leaves the rest as districts is useless for both purposes. The zoom is a deliberate action, and every time you make it, you say so — because the reader has to change what kind of question they are asking.

text
Announcing the move, in a review or an interview

  "That is the high-level shape: gateway, orders
   service, event stream, search indexer. The
   risky part is the indexer, so let me go a
   level down into it."

   ... module boundaries, schema, state machine,
       backfill algorithm ...

  "Coming back up: that changes the high-level
   picture in one way -- the indexer needs its own
   store for checkpoints, so it belongs on the
   diagram."

What we're doing: Descend from a high-level design into one component and come back up, noticing what the descent changed.

descend-and-return.txttext
HIGH LEVEL (settled)
  gateway -> orders service -> orders db
  orders service -> event stream -> search indexer
                                    -> search index
  Major flows: place an order; search orders.
  Boundaries: trust boundary at the gateway;
  network boundary on every arrow.

DESCEND -- "the indexer is the risky part"

  LOW LEVEL: search indexer
    modules: consumer, transformer, writer
    consumer: reads the event stream from a
      stored offset
    state: pending -> transformed -> indexed
                   -> dead-lettered
    schema (checkpoints):
      consumer_group PK, partition, offset,
      updated_at
    algorithm: bulk writes batched at 500 docs
      or 2 seconds, whichever comes first
    internal API: transformer.transform(event)
      -> Document | SkipReason

RETURN -- what the descent changed upstream

  1. The indexer needs a durable checkpoint
     store. That is a new box and a new arrow on
     the HIGH-LEVEL diagram -- it was not there.
  2. Dead-lettering needs somewhere to go. That
     is a queue, which is also high-level.

Neither existed before the descent. Both are
architecture, so both go back up rather than
staying buried in the component document.
10
The descent is chosen, not drifted into, and it is chosen because the previous level identified this component as the risk. That is the bottleneck-then-deep-dive ordering applied to documents rather than to a conversation.
22
The batching rule is a genuine low-level decision: it changes throughput and latency inside one component and is invisible from outside. It belongs here and nowhere else.
27
This is the return trip most designs skip. A new durable store is a new component dependency, so leaving it inside the indexer document means the architecture diagram is now wrong — and nobody reviewing the architecture will see it.

Why this works: The descent surfaced two facts that are architectural rather than internal, and the only way they reach the architecture is a deliberate return trip. Skipping it is how high-level diagrams quietly stop matching the system — not through a single wrong decision, but through a series of correct low-level ones that nobody carried back up.

Answering a high-level question with a low-level answer

Wrong

text
Q: "How does the search index stay in step
    with the orders database?"
A: "The transformer maps OrderCreated events
    onto a Document with a nested items array,
    and we batch at 500 docs..."

Better

text
Q: "How does the search index stay in step
    with the orders database?"
A: "The orders service writes an outbox row in
    the same transaction; an indexer consumes
    that stream and writes the index, so the
    index is derived and rebuildable."
    (then, if asked: the transformer details)

What you see: A reviewer asks about the architecture, receives implementation detail, and cannot tell whether the architecture is sound — so the question is either re-asked or, more often, dropped, and the architectural risk goes unexamined.

Why: The question named a relationship between two components, which is a high-level question, and the answer described the inside of one, which does not address it. Answering at the level asked keeps the review on the risk the reviewer was probing; the detail is still available a level down, once the shape has been agreed.

Three levels, and what changes between them

System context

the system, its users and the external systems it talks to

High-level design

components, owned stores, queues, boundaries, major flows

Low-level design

one component: modules, classes, schema, state machines, algorithms

Code

where the low-level design is realised, and where it stops being a document

  1. System context — the system, its users and the external systems it talks to
  2. High-level design — components, owned stores, queues, boundaries, major flows
  3. Low-level design — one component: modules, classes, schema, state machines, algorithms
  4. Code — where the low-level design is realised, and where it stops being a document

Three kinds of level mixing, and what each hides

Three kinds of level mixing, and what each hides
MixingLooks likeWhat it hides
Detail in a high-level designA diagram with one component's table columns on itThat the other components were never designed to the same depth
Architecture in a low-level designA component design that also decides service boundariesThat a boundary decision was made without an architecture review
Uneven depth across componentsThree services sketched, one specifiedThat the unspecified ones carry unexamined risk

The two consistency checks

The two consistency checks
DirectionCheckWhat a failure usually means
High → lowEvery component has a low-level design, or a note saying why notA component nobody has thought through yet
Low → highEvery store, queue and external call appears on the high-level diagramA dependency was taken without architectural review

Remember: Descend on purpose and say you are doing it: finish the level, name the component you are going into, work the detail, then come back up. One level per artefact — a high-level diagram that has grown a schema is no longer one. Then run both consistency checks: every high-level component has a low-level design or a note saying why not, and every store, queue and external call in a low-level design appears on the high-level diagram, because a missing one is usually a dependency nobody reviewed.

See also: what belongs in a high level design · what belongs in a low level design · the ten step sequence · the high level design checklist · architecture vs implementation

Advertisement