Search Tech Journey

Find topics, journeys and posts

back to blog
systemsadvanced 22m read

Designing for Scale · Requirements to Architecture

From a one-line prompt to a defensible architecture: separating functional from non-functional requirements, quantifying who/what/how-many, and refusing to draw a single box until the constraints are on the board.

The problem this post solves

Most bad architectures are not bad because someone chose the wrong database. They are bad because the design started before anyone agreed on what was being built, for how many people, with what tolerance for staleness, and at what cost of being wrong.

"Design a system to share short notes" is not a specification. It is a prompt. Between that prompt and a defensible architecture sits a step that is routinely skipped: converting an ambiguous sentence into a bounded problem with numbers attached. This post is about that step, and about why drawing boxes first is the most expensive habit in system design.

The claim is simple. The architecture is a consequence of the constraints. If the constraints are not written down, the architecture is a guess wearing a diagram.

First principles

Start from what a system actually is: a machine that moves data between people and storage, under limits.

From first principles
Start with the question
Why can't we start with the architecture diagram?
  1. 1
    A design is a set of choices between alternatives.
    forced by · Cache or no cache, sync or async, one region or many — each is a fork.
  2. 2
    A choice is only correct relative to a constraint.
    forced by · Async is right when the caller can tolerate delay, and wrong when it cannot. Nothing about the box tells you which.
  3. 3
    Therefore an unstated constraint makes the choice unfalsifiable.
    forced by · If nobody wrote down the latency target, no reviewer can say the choice is wrong — or right.
  4. 4
    Unfalsifiable designs fail late.
    forced by · They fail in load testing, or in production, rather than at the whiteboard where fixing them is free.
⇒ Therefore
Constraints first is not process ceremony. It is the only thing that makes the later decisions reviewable.

There are exactly two families of requirement, and conflating them is the most common early error.

Functional requirements describe what the system does. They are verbs. A user creates a note. A user retrieves a note by link. A note expires. You can write a test for each one, and the test passes or fails.

Non-functional requirements describe the conditions under which it must keep doing those things. They are adjectives and numbers. Ten thousand reads per second. Under two hundred milliseconds at the ninety-ninth percentile. Available when one region is on fire. Durable enough that losing a note is a serious incident rather than an annoyance.

Functional requirements determine what components exist. Non-functional requirements determine what shape they take. A note store and a note store that serves a hundred thousand reads per second across three continents contain the same nouns and almost none of the same architecture.

Common misconception
✗ What most people think
Non-functional requirements are the polish you add once the features work.
✓ What is actually true
Non-functional requirements are what decide the architecture. Features barely constrain it at all.
Why the myth is so sticky
Almost any feature set can be built on a single machine with a single database. It is the scale, latency, availability and durability targets that force sharding, replication, caching, queuing and multi-region — and each of those is structural, not something you bolt on later.
Prove it to yourself
Take any design and change only one number: reads per second, from a thousand to a million. If the diagram does not change, the diagram was never responding to the requirements.

The core model

The four-step frame below is not a trick. It is the same skeleton mature design documents follow, compressed into something that fits on a whiteboard.

The dotted lines matter as much as the solid ones. Discovering during capacity estimation that a feature implies a petabyte of hot storage is not a failure of the process; it is the process working. The cheapest place to discover an impossible requirement is immediately after writing it down.

Step one in practice: bounding the problem

Three questions convert a prompt into a problem.

Who uses it, and how many are there? Total registered users is nearly useless. Daily active users is the number that generates load. The ratio between them varies enormously by product and is the single most common place where an estimate goes wrong by an order of magnitude.

What do they do, and in what proportion? Every system has a read/write ratio, and it dictates the architecture more than almost anything else. A read-heavy system wants caching, read replicas and denormalisation. A write-heavy system wants partitioning, batching and back-pressure. A system where both are high wants a much larger budget.

What must never happen? This is the question people skip, and it is where durability and consistency requirements live. Losing a draft is bad. Losing a payment is unacceptable. Showing a stale follower count is fine. Showing a stale account balance is a regulatory event. The severity of the worst case sets the floor for how much machinery is justified.

Mental modelThe load triangle
Three corners: population (how many actors), frequency (how often each acts), and payload (how big each action is). Total load is roughly their product. Every scale problem is one corner being much larger than the design assumed.
  • Population growth is usually the slowest and most predictable corner.
  • Frequency is where product changes ambush you — a new notification can multiply it overnight.
  • Payload is where media features ambush you — text to images to video is three orders of magnitude.
🔔 Fires when you see
Reach for this whenever a system 'suddenly' fell over without a traffic spike. One corner moved.

Writing requirements that constrain

A requirement that cannot be violated is not a requirement. Compare:

"The system should be fast." This constrains nothing. Every design satisfies it, so it cannot be used to reject any design.

"Reads return in under 200 ms at p99, measured at the edge, for the top 90 percent of keys by traffic." This rejects designs. It rules out a cold read path that crosses an ocean. It implies a cache. It admits that the long tail is allowed to be slower, which is what makes it affordable.

The second version does three things the first does not: it names a percentile rather than an average, it names a measurement point, and it scopes itself to a subset of traffic. Each of those is a place where a vague target quietly becomes a much more expensive one.

Percentiles deserve particular care. An average latency target is nearly meaningless for user experience, because a page that makes twenty backend calls will hit the tail on most requests. If each call independently meets a p99 of 200 ms, the probability that a twenty-call page avoids the tail entirely is roughly 0.99 to the twentieth power — about 82 percent. Put differently, nearly one page load in five hits at least one slow call. Tail latency compounds; averages hide it.

Key points

    Capacity and constraints

    Capacity estimation earns its place only when a number changes a decision. An estimate that decorates the design without ruling anything out is wasted time, and reviewers can tell.

    The useful outputs are few. Requests per second, derived from daily actions divided by seconds in a day, then multiplied by a peak factor because traffic is never flat. Storage growth per year, derived from writes per day times bytes per write times retention. Bandwidth, derived from reads per second times response size. Working set, meaning the portion of data that is hot enough to justify keeping in memory.

    Each of those has a decision hanging off it. Requests per second decides whether one machine can serve the traffic. Storage growth decides whether the data fits on one node or must be partitioned. Bandwidth decides whether a content delivery network is a nice-to-have or load-bearing. Working set decides the cache tier size, and therefore much of the cost.

    The arithmetic should stay symbolic and round. Precision is false comfort here: the input assumptions carry far more error than the multiplication does. What matters is the order of magnitude and the decision it forces.

    The tradeoff
    How much estimation is enough before moving to the design?
    Skip estimation, design from intuition
    + you gain Fast to a diagram; feels productive.
    − you pay Every subsequent choice is unjustifiable, and structural errors surface only under load.
    pick when Only when the system is genuinely small and single-node, and you can say so out loud.
    Estimate only what forces a decision
    + you gain Each number earns its place; the design becomes reviewable against the numbers.
    − you pay Requires the discipline to stop, which is harder than it sounds once the arithmetic is flowing.
    pick when Default. Compute a number, then immediately state which choice it just made.
    Full detailed capacity model
    + you gain Thorough, and appropriate for a real production plan with a budget attached.
    − you pay Slow, and the precision is illusory when the input assumptions are themselves estimates.
    pick when When money is being committed, not when the architecture is still being chosen.

    Architecture

    Only now do the boxes appear, and they appear as answers to the constraints rather than as a remembered template.

    Every element in that diagram should be traceable to a requirement. The edge exists because of a latency target and a geographic spread of users. The cache exists because of a read/write ratio and a working set that fits in memory. The queue exists because some work was identified as tolerating delay, which is a statement about requirements, not about technology. The replicas exist because of an availability target and a read volume that exceeds one node.

    If a component cannot be traced back to a constraint, it is decoration, and it should be removed. Extra components are not free: each one adds a failure mode, an operational burden and a consistency question.

    The reverse check is equally important. Walk the non-functional list and ask which part of the diagram satisfies each entry. A durability requirement with no replication or backup answering it is a hole. An availability requirement with a single point of failure in the path is a hole. Holes found at the whiteboard cost nothing.

    Choosing the deep dive

    A design review has time for one component to be examined properly. Choosing which one is itself a judged decision, and the right choice is the component where the design is most likely to be wrong — usually the one under the most extreme constraint, or the one where a trade-off was asserted without justification.

    The wrong choice is the component the designer happens to find most interesting.

    You can now
    • Separate functional from non-functional requirements and explain why the latter drive structure.
    • Convert a vague prompt into bounded requirements using population, frequency and payload.
    • State a latency requirement that can actually reject a design.
    • Derive requests per second, storage growth, bandwidth and working set — and name the decision each one forces.
    • Trace every component in a diagram back to the constraint that justifies it, and delete the ones that fail the trace.

    Failure modes

    Designing from a remembered template. Producing the same diagram regardless of the prompt is the clearest signal that requirements were never engaged with. The tell is a component that cannot be justified when questioned.

    Averages instead of percentiles. A design that meets an average latency target can still be slow for most users of a multi-call page, because tail latency compounds across calls.

    Confusing registered users with active users. This single substitution routinely produces estimates that are wrong by one or two orders of magnitude, in whichever direction is least convenient.

    Treating peak as average. Traffic is not flat. Designing to the daily mean guarantees the system is under-provisioned at exactly the moment it matters most.

    Accepting an unbounded requirement. "All historical data must be instantly queryable" and "the system must never lose a write" are not requirements until someone attaches a retention window and a durability target. Unbounded requirements silently import unbounded cost.

    Deferring the consistency question. Whether a read may return stale data is a product decision that determines replication strategy, cache invalidation and multi-region topology. Leaving it implicit means it gets decided by accident, usually by whichever component was built first.

    Trade-offs that matter

    The recurring tension in this phase is between bounding the problem tightly enough to design against, and bounding it so tightly that the design cannot survive a plausible change in the product.

    Over-constraining produces a system exquisitely fitted to today's numbers that requires rearchitecting when one assumption shifts. Under-constraining produces a system with no defensible choices in it at all, which is worse: at least the over-fitted system works today.

    The practical resolution is to state which assumptions are load-bearing. If the design depends on the read/write ratio staying above roughly one hundred to one, say so explicitly. That sentence turns a hidden fragility into a monitored one, and it tells whoever inherits the system which number to watch.

    Similarly, requirements should record what was deliberately excluded. A design that says "cross-region strong consistency is out of scope; regional reads may be up to a few seconds stale" is stronger than one that quietly omits the question, because the omission is now a decision someone can revisit rather than a landmine someone will discover.

    Four worked cases

    The frame is only worth something if it survives contact with different prompts. What follows are four prompts run through the same four steps. The point is not the answers; it is that the same procedure produces different architectures, because the constraints differ.

    Requirements. Functional: create a short code for a URL, resolve a code to a URL, expire a code, count resolutions. Non-functional: resolution p99 under 50 ms at the edge, creation p99 under 300 ms, resolution must survive a region loss, a lost creation is an annoyance, a wrong resolution is an incident.

    Numbers. Say 1M creations/day and 500M resolutions/day. That is roughly 12 writes/s and 5,800 reads/s average, call it 3x at peak, so ~17,000 reads/s. Read/write ratio ≈ 500:1. Payload is tiny — 500 bytes/row, so 1M/day is 500 MB/day, 180 GB/year. The whole hot set is a few GB.

    What the numbers decide. A 500:1 ratio and a working set that fits in RAM means the cache is load-bearing, not decorative. 180 GB/year means a single primary node is fine for years — do not shard. Resolution surviving a region loss plus a tiny dataset means full replication everywhere beats partitioning. "A wrong resolution is an incident" means codes must never be reused, which kills random generation without a uniqueness check and pushes toward a monotonic counter with base-62 encoding.

    Case B — Group chat

    Requirements. Functional: send a message to a room, receive messages in near-real-time, fetch history, show delivery state. Non-functional: delivery p95 under 200 ms end-to-end, a delivered message must never be lost, ordering must be consistent per room, history may be seconds stale.

    Numbers. 10M DAU, 40 messages sent/user/day = 400M writes/day ≈ 4,600 writes/s average, ~14,000 at peak. But fan-out is the real number: average room size 8 means ~110,000 deliveries/s at peak. Storage: 400M × 1 KB = 400 GB/day, 146 TB/year.

    What the numbers decide. Writes are now comparable to reads — the cache stops being the story and the write path becomes it. 146 TB/year forces partitioning on day one, and "ordering consistent per room" makes room_id the obvious partition key. Persistent connections at 10M DAU means a connection tier separate from the logic tier, because those scale on different axes (memory vs CPU). "Never lost" means acknowledge only after durable commit, never after enqueue.

    Case C — News feed

    Requirements. Functional: post, follow, render a ranked feed. Non-functional: feed p95 under 300 ms, feed may be up to a minute stale, a dropped post is bad but not an incident.

    Numbers. 100M DAU, 20 feed loads/day = 2B reads/day ≈ 23,000/s, ~70,000 peak. Writes: 5M posts/day ≈ 58/s. Ratio ≈ 400:1 — but with fan-out, since one post by a user with 10M followers touches 10M feeds.

    What the numbers decide. The staleness allowance of one minute is the single most valuable requirement in the list: it makes precomputation legal. Fan-out-on-write becomes viable for ordinary users, but the celebrity tail breaks it — 10M writes for one post. So the architecture is hybrid: precompute for the median user, compute-at-read for the high-fan-out tail. That hybrid is not a clever trick; it falls directly out of the follower distribution being heavy-tailed.

    Case D — Metering and billing

    Requirements. Functional: ingest usage events, aggregate per account per period, produce an invoice. Non-functional: an event must never be double-counted and never dropped, aggregates may lag by minutes, invoices must be reproducible years later.

    Numbers. 2B events/day ≈ 23,000/s, 70,000 peak. 200 bytes/event = 400 GB/day raw.

    What the numbers decide. "Never double-counted" dominates everything else. It forces idempotency keys on ingest, exactly-once semantics at the aggregation boundary, and an immutable raw event log as the system of record. "Reproducible years later" forbids destructive aggregation — you keep the raw log and recompute, rather than trusting a running total. Notice that the QPS here is similar to Case C but the architecture shares almost nothing with it, because the correctness requirement differs.

    Short link

    500:1 read/write

    • **Wrong resolution = incident**
    • Cache is load-bearing
    • No sharding for years
    • Replicate everywhere

    Group chat

    Write-heavy w/ fan-out

    • **Never lose a delivered msg**
    • Partition by room_id
    • Split connection tier
    • Ack after durable commit

    News feed

    400:1 + heavy tail

    • **1 min staleness allowed**
    • Precompute legal
    • Hybrid push/pull
    • Celebrity tail is the design

    Metering

    23k events/s

    • **No double-count, no drop**
    • Immutable raw log
    • Idempotency keys
    • Recompute, never trust totals
    Same four steps, four different answers. The requirement in bold is the one that forced the shape.

    The requirement that decides the shape

    Across those four, one requirement in each did most of the work — and in no case was it the QPS number. Cases C and D have nearly identical throughput and almost nothing in common structurally.

    This is worth stating as a rule: throughput decides how many machines; correctness and staleness decide what the machines do. Designers who lead with QPS end up with a well-sized version of the wrong system.

    The staleness allowance in particular behaves like a currency. A system permitted one minute of staleness can precompute, cache aggressively, replicate asynchronously and batch its writes. A system permitted zero staleness gives up all four. When a stakeholder says "it must be real-time," the correct response is to ask what breaks at one second, then ten, then sixty — the answer is usually a number, and that number is worth more to the design than any other sentence in the room.

    0.0025050075010000 ms100 ms1 s10 s60 s5 minstaleness budgetrelative cost per read
    Cost per read against the staleness the product will tolerate. The knee is where precomputation becomes legal — buying a few seconds of staleness is usually the cheapest architectural decision available.

    Tracing the boxes back

    The reverse check deserves a picture, because it is the step most often skipped. Every component below is annotated with the requirement it exists to satisfy. Delete the requirement and the component must go with it.

    replicationreadsClientsgeo-spreadEdge / CDN← p99 latency + geographyLoad balancer← availability targetStateless tier← horizontal scale on QPSCache← read/write ratio + working setPrimary store← durability floorAsync queue← work that tolerates delayWorkers← decoupled throughputReplicas← availability + read volume
    Each box traces to one constraint. If you cannot name the constraint, the box is decoration.

    Numbers worth memorising

    Estimation is only fast if the constants are already in your head. These are the ones that actually change decisions, rounded hard on purpose.

    Time. 86,400 seconds in a day — round to 100,000 and your QPS estimate is 15% low, which is inside the error of every other assumption you made. 1M requests/day ≈ 12/s. 100M/day ≈ 1,200/s. 1B/day ≈ 12,000/s.

    Peak factor. Consumer traffic peaks at roughly 2–3x the daily mean; a single-timezone product can hit 5x. Say which you are assuming.

    Latency floors. Memory reference ~100 ns. SSD random read ~100 µs. Same-datacenter round trip ~0.5 ms. Cross-continent round trip ~150 ms — that last one is a hard floor set by the speed of light in fibre, and no amount of engineering removes it. If the requirement is 50 ms globally and the data lives in one region, the requirement is already impossible and someone should say so in the first five minutes.

    Storage. 1 KB × 1M/day = 1 GB/day ≈ 365 GB/year. A single modern node comfortably holds single-digit TB. So roughly: under ~5 TB/year, do not shard.

    Tail compounding. A page that makes 10 independent calls, each with a p99 of 100 ms, has roughly a 10% chance of at least one slow call — so the page p90 is what the call p99 was. This is why per-service p99s do not add up to a good user experience, and why fan-out width is itself a latency decision.

    Common misconception
    ✗ What most people think
    If each service meets its p99 latency target, the page will be fast.
    ✓ What is actually true
    Tail latency compounds across fan-out. Ten parallel calls at p99 = 100 ms means ~10% of page loads wait 100 ms or more. Either reduce the number of calls, hedge requests, or set per-call targets stricter than the page target.
    Prove it to yourself
    Count the fan-out width of your slowest page, then compute 1 - 0.99^n. If that exceeds your error budget, the per-call target is wrong.

    Where this frame breaks

    It would be dishonest to present this as universal. It is a frame for systems whose difficulty is scale, and there are systems whose difficulty is something else.

    Regulated and correctness-first systems. In payments, ledgers and clinical systems the binding constraint is auditability and invariants, not throughput. A ledger doing 50 transactions per second can still be the hardest system in the building. Starting from QPS there is a category error; start from the invariants that must never be violated and design the audit trail first.

    Latency-floor systems. In trading or real-time control, the requirement is a hard ceiling on the tail, not a percentile of a distribution — and the architecture becomes about removing hops, not adding them. Most of the toolkit above (queues, caches, replicas) adds variance and is therefore disqualified.

    Systems where the requirement is genuinely unknown. For a product still finding its shape, over-specifying the non-functional requirements is a way of pretending to knowledge you do not have. The right move is a deliberately simple design plus written triggers: "we shard when the primary passes 2 TB", "we split the connection tier when we pass 200k concurrent". That converts an unknown into a monitored threshold rather than a guess.

    Small systems. If the honest answer is that one machine serves the traffic with room to spare, the correct design is one machine, and the mark of seniority is saying so rather than producing a distributed system to look thorough.

    Companion deep report

    A longer working paper accompanies this post: full arithmetic for all four cases, the constants table, a six-dimension self-grading rubric, a catalogued failure-mode list, and a five-problem timed drill set.

    What to carry forward

    The frame is small enough to hold without notes: bound the problem, quantify it, design against the quantities, then examine the weakest component properly.

    The discipline underneath it is smaller still. Before drawing anything, write down the numbers the drawing has to satisfy. After drawing anything, check that every box traces back to one of them. Most design failures are a violation of one of those two sentences.

    Capacity estimation, which is where the numbers actually come from, is involved enough to deserve its own treatment — including how to keep the arithmetic honest and how to tell an estimate that constrains from one that merely impresses.