Foundations: Scoping and Estimating
Last verified against its sources on 23 September 2026
Every system design interview starts the same way: turn a one-line prompt into a short, defensible scope. This module covers the two moves that make that possible — splitting functional requirements (what the system does) from non-functional ones (how well it does it, stated as a number), and converting a user count into the order-of-magnitude traffic, storage and bandwidth figures that decide whether a single database node is plausible. It then covers the API and data model those numbers point to: resource-oriented endpoints, idempotency for safe retries, and a schema shaped by the read/write ratio rather than habit.
Scoping the Problem
- Separate a system's functional requirements from its non-functional constraints before proposing a design.
- State a non-functional requirement as a measurable target (an SLI and an SLO), not a vague adjective.
In a system design interview, "requirements" splits into two different kinds of statement, and mixing them up is the single most common way candidates lose time in the first five minutes. A functional requirement names an action the system performs — a user posts a photo, a service redirects a short link, a rider requests a car. A non-functional requirement names how well the system has to perform that action — how many requests per second, how long a response may take, how available the service must stay, whether a lost write is acceptable. Interviewers are not grading whether you remember a diagram; they are grading whether you can turn a vague prompt into a short list of both kinds of statement before you draw a single box.
Take "design a URL shortener," a prompt most candidates have heard before. The functional list is short: accept a long URL and return a short code, redirect a short code to its long URL, and — often — let a user choose a custom alias or set an expiry. Everything else is a question, not an assumption: does a link need to work forever, or can it expire? Does the same long URL always produce the same short code, or is a fresh code fine? Each answer changes the design. The non-functional list is where the interview is actually won: redirects will vastly outnumber creations, so read latency matters far more than write latency; a redirect that takes half a second feels broken, while a create call that takes half a second does not.
A non-functional requirement only does its job once it names a number. Site reliability engineering gives this a vocabulary worth borrowing: a service level indicator (SLI) is the metric you actually measure — redirect latency, error rate, availability — and a service level objective (SLO) is the target value you set for it, such as "p99 redirect latency under 200 ms" or "99.9% of redirects succeed." A service level agreement (SLA) is what happens if you miss it — a refund, a penalty, a support escalation. "The system should be fast" is not a requirement; "p99 latency under 200 ms measured at the load balancer" is. Availability targets are usually written as a run of nines — 99.9%, 99.99% — because each extra nine is an order of magnitude harder to hold, not because the digits themselves are special.
An interviewer writes "the system should scale well." What is missing before this counts as a usable non-functional requirement?Answer it yourself first, then open this.
A measurable target and the load it applies to — e.g. "handle 5,000 writes/sec with p99 latency under 300 ms," not just "scale well."
Back-of-the-Envelope Estimation
- Convert a stated user base and usage pattern into an order-of-magnitude queries-per-second figure.
- Estimate storage and bandwidth from a data model and a retention period, and sanity-check both against a latency budget.
Back-of-the-envelope estimation is not about precision — it is about finding out, in under two minutes, whether a design is off by an order of magnitude before you commit to it. The method is always the same: start from a number the prompt gives you or you can defend as an assumption (daily active users, for instance), round aggressively to the nearest convenient power of ten, and multiply through to the figure you actually need. A candidate who says "100 million daily users, each posting roughly once a day, so about 100 million writes a day, call it 1,200 writes a second" has done more useful work in fifteen seconds than one who spends three minutes computing 1,157 writes a second to the exact decimal. The rounded number is what drives every later decision — whether one database node is plausible, whether a queue is needed, whether a cache is worth the complexity.
The standard conversion is daily active users (DAU) times actions per user per day, divided by the number of seconds in a day — about 86,400, worth memorizing as "roughly 100,000" for a first pass. 100 million DAU at one write each gives 100 million ÷ 86,400 ≈ 1,160 writes/sec average. Average is rarely the number that matters: traffic is not flat across the day, and a peak-to-average ratio of 2–3× is a reasonable default to state explicitly rather than assume silently. A system built for "1,200 writes/sec average" should actually be sized for something closer to 3,000 writes/sec at peak. Reads usually dwarf writes — a social feed might see 100 reads for every write — so compute the two separately rather than assuming symmetry; a read-heavy design wants a cache and read replicas long before it needs write sharding.
Storage follows the same pattern: estimate the size of one record, multiply by how many are created per day, multiply by how long they must be kept. A short-link record at roughly 500 bytes, created 100 million times a day, kept for five years, comes out to about 500 B × 100M × 365 × 5 ≈ 90 TB — enough to know immediately that a single machine's disk will not hold it, and that partitioning is a real requirement, not a nice-to-have. Bandwidth is QPS times average payload size in each direction; a redirect service returning a few hundred bytes at 1,200 requests/sec needs well under 1 MB/sec of egress, which rules out network bandwidth as a bottleneck and points the conversation toward where the real limit sits — usually disk I/O or a single database connection pool.
10 million DAU each perform 5 read actions a day. What's a reasonable order-of-magnitude estimate for average reads per second?Answer it yourself first, then open this.
10M × 5 ÷ 86,400 ≈ 580/sec — round to "a few hundred to a thousand reads/sec," and remember peak traffic runs several times higher.
Designing the API
- Design a resource-oriented API where nouns are resources and HTTP methods carry the verb.
- Use idempotency keys to make a non-idempotent operation safe to retry.
Once the requirements are scoped, the API is the contract the rest of the design has to satisfy, and the fastest way to make it look considered is to model resources, not actions. A URL shortener's API is not "createLink" and "getLink" — it is a links resource: POST /links creates one, GET /links/{code} reads one, DELETE /links/{code} removes one. HTTP already carries the verb, so the path only has to name the noun. This matters beyond style: HTTP defines shared behavior per method that clients, proxies and browsers all rely on. GET is defined as a safe method — a client does not expect it to change server state — which is why it can be retried, cached, and prefetched without asking permission. POST, PUT and DELETE are not safe; a client should only issue them because the user asked for that specific change.
HTTP also defines idempotent methods: a method is idempotent if sending the same request twice has the same effect on the server as sending it once. PUT, DELETE, and every safe method are idempotent by this definition; POST is not. That gap matters on a flaky network: a client that times out waiting for a POST /links response genuinely cannot tell whether the link was created or the response was merely lost, and retrying blindly risks creating a duplicate. The common fix is an idempotency key — the client generates a unique token once and sends it with every retry of the same logical request; the server stores which keys it has already processed and returns the original result for a repeat, instead of creating a second resource. PUT sidesteps the problem structurally: PUT /links/{code} with a client-chosen code can be retried safely without any extra key, because setting a resource to the same state twice is by definition idempotent.
Fielding's dissertation, which coined REST, ties two more constraints to this same idea. A stateless server keeps no session state between requests — every request carries everything needed to understand it (an auth token, not a server-side session ID pinned to one machine) — which is what lets you add a second server behind a load balancer without sticky sessions. And a response that is cacheable must say so explicitly, because a client, proxy or CDN is only allowed to reuse a stored response when the server has labelled it safe to reuse. A redirect service benefits enormously from both: a stateless GET /r/{code} can be served from any node, and a redirect that rarely changes can carry a cache header that keeps most traffic from ever reaching the database at all.
A mobile client's create-link request times out. The client retries with the same idempotency key. What should the server do differently from the first attempt?Answer it yourself first, then open this.
Recognize the key as already processed and return the original link's result — it must not create a second link.
Modelling the Data
- Choose a schema shape (normalized vs denormalized) based on the system's read and write pattern, not habit.
- Add an index that matches a stated query pattern, and explain what it costs on writes.
A schema is a bet on which queries will be common, and the two default shapes bet in opposite directions. A normalized schema stores each fact once — a users table, a links table with a user_id foreign key — and reconstructs a full view with a join; it keeps writes cheap and avoids the update-in-three-places bug, but every read that needs a link and its owner's email now costs a join. A denormalized schema copies data where it will be read — storing the owner's display name directly on the link row — which makes that read a single lookup at the cost of updating every copy when the name changes. Neither is "more correct"; the right choice follows directly from the ratio you estimated earlier. A read-heavy redirect service denormalizes aggressively; a ledger that must never show two different balances for the same account normalizes and pays the join.
An index trades write cost for read speed by keeping a separate, sorted structure that points back to the table's rows, so a lookup by an indexed column no longer has to scan every row. PostgreSQL's own documentation frames the trade-off plainly: without an index the system has to scan the entire table row by row to find matching entries, which is fine for a small table and expensive once the table has millions of rows. That speed is not free — every INSERT or UPDATE on an indexed column has to update the index too, so an index earns its place only when a real, named query pattern reads by that column often enough to justify slower writes. code on the links table is an obvious index — every redirect reads by it. created_at might not be, unless a real query actually filters or sorts by creation time.
A redirect service always looks up a link by its short code, and never queries by creation date. Should created_at get an index?Answer it yourself first, then open this.
Not unless a real query needs it — an unused index still slows every write for no read benefit.
Sources
- Service Level Objectives — Site Reliability Engineering
- Latency Numbers Every Programmer Should Know
- RFC 9110: HTTP Semantics
- Architectural Styles and the Design of Network-based Software Architectures — Chapter 5: Representational State Transfer (REST)
- PostgreSQL Documentation — Chapter 11. Indexes, 11.1. Introduction