Filter concepts by levelShowing all levels.

System Design · Section 101

Low-Level Design Checklist

Level
intermediate
Read
13 min
Concepts
1

Eleven items, run against one component once its design exists. They group into four questions. What exists: entities, interfaces, class responsibilities — the nouns, the contracts between them, and who owns each rule. How it changes: state machines, database schema, indexes — the legal transitions, the shape that outlives the code, and the access path behind each named query. What goes wrong: error handling, concurrency controls, retries — what a caller sees on failure, what happens when two requests touch the same row, and which operations are safe to run twice. How it holds together: module boundaries and testability. Two items carry most of the value. The schema binds every future writer, because changing it later means migrating rows that already exist, so deferring it as "an implementation detail" does not postpone the decision — it makes it silently in whatever shape the first migration took. The state machine is the cheapest gap-finder on the list, because an unenumerated case shows up as a missing arrow you can point at: the classic one is a hold that expires while its payment is in flight, where both paths are individually correct and only collide on a timer. Scope is what makes the list work. Run across a whole system, "concurrency controls" honestly answers "we use transactions", which is true and hides every place there are none; run against one component, it has to name a row, a lock and the status code the loser sees. Each item still gets one of three answers — handled in this specific way, deliberately not handled because X, or not yet considered.

System Design overview

What is true here

  1. One component per pass — at system scope every item collapses into a technology name and the list stops finding anything.
  2. Four groups: what exists, how it changes, what goes wrong, how it holds together.
  3. The schema outlives the code and binds every future writer; deferring it makes the decision silently rather than later.
  4. A state machine turns an unasked case into a visibly missing arrow — expiry racing payment is the standard find.
  5. Concurrency and retries fail only under load, which makes a design review the last cheap place to catch them.

What you will be able to do

  • Run the eleven items against a single component and produce named, disagreeable answers rather than technology names
  • Draw a component's state machine to the point where every terminal state and every race has an arrow or an explicit decision
  • Move a correctness rule out of application code and into a schema constraint that binds writers you have not met
  • Explain why "we use transactions" and "the client retries" are non-answers, and what a reviewable version of each looks like

The checklist

Eleven items in four groups, scoped to one component, three permitted answers each.

The eleven-item low-level design checklist

coreintermediate

Eleven items, run against one component after you have designed it. They fall into four groups. What exists: entities, interfaces, class responsibilities — the nouns, the contracts between them, and who owns each rule. How it changes: state machines, database schema, indexes — the legal transitions, the shape that outlives the code, and the access paths that make queries survive growth. What goes wrong: error handling, concurrency controls, retries — what a caller sees on failure, what happens when two requests touch the same row, and which operations are safe to run twice. How it holds together: module boundaries and testability. The schema and the state machine carry the most weight. A schema binds every future writer, because changing it later means migrating data that already exists. A state machine turns "what if the payment succeeds after the order was cancelled" from an unasked question into a missing arrow you can see. Where the high-level checklist finds missing behaviour, this one finds missing rules — the transitions, constraints and concurrent cases nobody enumerated. Each item gets the same three answers as any checklist: handled in this specific way, deliberately not handled because X, or not yet considered.

Think of it as

A building inspection after the walls are up, not the architect's sketch. The sketch already said where the rooms go; the inspection asks the questions that only have answers once something concrete exists — is this load-bearing, what is the wiring rated for, what happens when the water is on and the power is out. Every item here is that kind of question: it has no useful answer against a box on a diagram, and a very specific one against a component someone is about to build.

text
Running the list on ONE component, after designing it

  entities             handled  Reservation, Seat, Hold
  interfaces           handled  4 methods, documented
  responsibilities     handled  expiry owned by sweeper only
  state machine        handled  5 states, 6 arrows, 2 terminal
  schema               handled  3 tables, keys + constraints
  indexes              handled  2, each tied to a named query
  error handling       partial  no answer for provider 5xx
  concurrency          NOT YET CONSIDERED    <-- the find
  retries              handled  idempotency key on POST
  module boundaries    handled  domain has no HTTP imports
  testability          handled  expiry is a pure function

Two items to fix, and the second one is the
one that only fails when two people click at
the same moment.

What we're doing: Run the "how it changes" and "what goes wrong" groups against a seat-hold component that already has a diagram.

seat-hold-review.txttext
The component: seat holds for a ticketing
service. Two boxes on the high-level diagram,
one database. It looks small.

STATE MACHINE
  Q: draw the states and every arrow.
  A: held -> confirmed -> fulfilled, and
     held -> expired.
  Q: what happens to a hold that expires
     while payment is in flight?
  A: ... it expires. Then payment succeeds.
  Gap: a missing arrow. The money arrived for
  a seat the system already gave away. Either
  expiry blocks while payment is pending, or
  there is a refund arrow. Both are designs;
  neither existed.

SCHEMA
  Q: what stops two holds on one seat?
  A: the application checks before inserting.
  Gap: a check-then-insert is not a rule, it
  is a race. The rule belongs in the schema:
  a partial unique index on seat_id for rows
  in (held, confirmed).

CONCURRENCY
  Q: two users click the last seat in the
     same millisecond. What happens?
  A: one of them gets it.
  Q: which one, and what does the other see?
  A: ... unclear.
  Gap: no lock ordering, no defined loser
  response. The answer "409, seat taken" is
  fine — but it has to be chosen.

RETRIES
  Q: the client times out and retries the
     hold. What happens?
  A: they get a hold.
  Q: one hold, or two?
  Gap: two. Nothing makes POST /holds
  idempotent, so a flaky network quietly
  double-books inventory.

Four gaps. The diagram was correct.
10
The expiry-versus-payment race is the single most common missing arrow in reservation systems, because both paths are correct in isolation and only collide on a timer.
21
Check-then-insert reads as a rule and behaves as a race. Moving it into a constraint means the database enforces it for every writer, including the one someone adds next year.
37
A retry that creates a second hold is not a bug in the client. It is a missing decision in this component, and the checklist is the moment it gets made.

Why this works: None of these four are architecture mistakes. They are rules nobody was prompted to write down, and every one of them fails only under a condition that is hard to reach by hand — a timer expiring mid-payment, two clicks in the same millisecond, a network timeout on a write. That is exactly the class of defect a low-level review is cheap at catching and production is expensive at catching.

Running the low-level list across the whole system at once

Wrong

text
# "Entities?"  -> "users, orders, payments,
#                  seats, notifications..."
# "Concurrency?" -> "we use transactions"
# One pass, eleven answers, every one of them
# true about the system and useful about
# nothing in it.

Better

text
# One component per pass.
# "Concurrency, in the hold component?"
#   -> "SELECT FOR UPDATE on the seat row;
#       the loser gets 409 seat_taken"
# Same eleven questions. Answers you can
# disagree with, because they name a row, a
# lock and a status code.

What you see: Every item is answered, the review takes twenty minutes, and no gap is found — because at system scope the honest answer to "concurrency controls" really is "transactions", and that sentence hides every place there are none.

Why: These items are only answerable against a specific set of rows, states and callers. Widen the scope and each one collapses into a technology name, which is always true and never reviewable. Scope is what makes the list produce findings rather than agreement.

Eleven items in four groups

What exists

Entities

Interfaces

Class responsibilities

How it changes

State machines

an unasked case is a missing arrow

Database schema

outlives the code

Indexes

What goes wrong

Error handling

Concurrency controls

only fails under load

Retries

How it holds together

Module boundaries

Testability

  • What exists
    • Entities
    • Interfaces
    • Class responsibilities
  • How it changes
    • State machines — an unasked case is a missing arrow
    • Database schema — outlives the code
    • Indexes
  • What goes wrong
    • Error handling
    • Concurrency controls — only fails under load
    • Retries
  • How it holds together
    • Module boundaries
    • Testability

The eleven items, grouped by the question each answers

The eleven items, grouped by the question each answers
GroupItemsThe question it forces
What existsEntities · Interfaces · Class responsibilitiesWhat are the nouns, what contract does each expose, and who owns each rule?
How it changesState machines · Database schema · IndexesWhat transitions are legal, what shape stores them, and what path serves each read?
What goes wrongError handling · Concurrency controls · RetriesWhat does a caller see on failure, what happens on a collision, and what is safe to run twice?
How it holds togetherModule boundaries · TestabilityWhat may import what, and can each rule be exercised without the whole system running?

The same item, answered two ways — only one of them is reviewable

The same item, answered two ways — only one of them is reviewable
ItemRecords a category (not an answer)Records a decision (reviewable)
State machineReservations have a status fieldheld → confirmed → fulfilled; held → expired; confirmed → refunded. No arrow out of expired.
SchemaStored in Postgresreservations(id, seat_id, user_id, state, held_until); UNIQUE(seat_id) WHERE state IN (held, confirmed)
IndexesWe will add indexes as neededIndex on (held_until) for the expiry sweep; the partial unique index above serves the seat lookup
ConcurrencyWe use transactionsSELECT … FOR UPDATE on the seat row inside the hold transaction; second caller blocks, then sees the unique violation
RetriesThe client retriesPOST /holds takes an Idempotency-Key; a repeat returns the original hold rather than taking a second seat
TestabilityIt has unit testsExpiry is a pure function of (held_until, now), so every boundary case is testable without a clock or a database

Remember: Eleven items, one component, run after the design exists: entities, interfaces and class responsibilities (what exists); state machines, schema and indexes (how it changes); error handling, concurrency controls and retries (what goes wrong); module boundaries and testability (how it holds together). The schema and the state machine earn the most, because a schema binds every future writer and a state machine turns an unasked case into a visibly missing arrow. Same three answers as any checklist — handled this way, deliberately not, or not yet considered.

See also: the high level design checklist · what belongs in a low level design · status and lifecycle modeling · optimistic vs pessimistic · idempotency keys for post requests · holds expiry and avoiding double booking

Advertisement