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.

    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.