System Design Methodology and Trade-off Analysis Questions
The end-to-end approach to an open-ended design problem and the judgment that resolves it: clarifying scope and constraints, gathering functional and non-functional requirements, capacity and back-of-envelope estimation, and mapping requirements to a high-level architecture, then reasoning explicitly about competing options on cost, complexity, latency, and reliability to defend a choice. Covers driving a design interview from ambiguity to a proposal, trade-off frameworks, decision-making under uncertainty and incomplete information, reversible-versus-irreversible decisions, and defending choices under scrutiny. The process-and-judgment skill underneath every system-design case study.
Compare Reserved Instances (or Savings Plans), On-Demand pricing, and Spot/Preemptible instances. When is each pricing model the right choice, and how would you mix them for one workload?
Sample Answer
Direct answer
Match the pricing model to how predictable and interruptible the workload is: pay full price (on-demand) for unpredictable or short-lived work, commit to a discount (a reserved instance or savings plan) for the steady baseline you know you'll run for a year or more, and reach for the deep spot or preemptible discount only when the work can be killed and resumed without harm. Real workloads mix all three: a steady floor on committed pricing, a growth buffer on on-demand, and interruption-tolerant batch work on spot.
Structured elaboration
When each fits
| Model | Best for | Discount driver | Risk |
|---|---|---|---|
| On-demand | Unpredictable, short-lived, dev/test workloads | None (list price) | None beyond cost |
| Reserved instance / savings plan | A known steady baseline over a 1-3 year horizon | Commitment to spend | Overcommit if load is more variable than assumed |
| Spot / preemptible | Fault-tolerant, stateless, batch, or resumable work | Provider can reclaim capacity anytime | Interruption; never for a single-master database or anything without a fallback |
Mixing for one workload
Split the workload into a steady floor, a variable middle, and interruption-tolerant batch, and price each piece with the model that fits it, rather than choosing one model for the whole workload.
Worked example (illustrative unit prices, not vendor-quoted rates)
Assume on-demand costs $0.10 per instance-hour, a committed baseline (reserved instance or savings plan) costs $0.06 per instance-hour (40% off), and spot averages $0.03 per instance-hour (70% off, an illustrative average across reclaim risk).
Baseline: 20 instances running 24/7 (730 hours/month):
on-demand cost=20×730×0.10=$1,460/month committed cost=20×730×0.06=$876/monthVariable batch work: 10 additional instances running 8 hours/day on average (240 hours/month) on spot:
spot cost=10×240×0.03=$72/month on-demand equivalent=10×240×0.10=$240/monthBlended monthly cost with the mix versus all on-demand:
mixed=876+72=$948/month all on-demand=1,460+240=$1,700/month savings=1,7001,700−948≈44%Trade-offs & pitfalls
- Never put a single-master, stateful database on spot; there is no fallback for the primary if it is reclaimed mid-write.
- Overcommitting reserved or savings capacity beyond your actual steady floor turns a discount into waste; size the commitment to your measured baseline, not your hoped-for growth.
- Spot needs operational work to be safe: checkpointing, diversified instance types and zones so a single reclaim wave doesn't take out your whole batch fleet, and an automatic fallback to on-demand.
- Managing multiple pricing commitments has its own administrative overhead (tracking utilization, renewal timing); factor that into whether the discount is worth it at small scale.
Product tells you the system must 'handle spikes.' What clarifying questions and metrics would you ask for to turn that into a measurable constraint you can actually design against?
Sample Answer
Direct answer
Turn "handle spikes" into numbers by asking for the spike multiplier over baseline, its duration and arrival shape, the peak concurrency it implies, and what is allowed to degrade versus what must stay within the service-level agreement (SLA) during it. Those four answers are what actually let you size autoscaling, connection pools, and a degradation plan; without them, "handle spikes" is a feeling, not a requirement.
Structured elaboration
The four questions that make it measurable
| Ask | Why it matters | What it changes in the design |
|---|---|---|
| Spike multiplier (for example 5x, 10x baseline) | Sets the capacity ceiling | Autoscaling target and reserved headroom |
| Duration (seconds, minutes, hours) | Short spikes need fast reaction or buffering; long ones need sustained capacity | Whether you lean on autoscaling reaction time or pre-provisioned warm pools |
| Arrival shape (sudden burst, ramp, or periodic) | Changes what absorbs the shock | Rate limiting and queueing versus scheduled pre-scaling |
| What must stay within SLA versus what can degrade | Defines the failure mode you design for | A graceful-degradation plan (partial feature disabling, cached fallback, explicit error responses) instead of an undifferentiated outage |
The general skill, applied to a different vague ask
The same discipline works on any vague requirement, not just traffic spikes. "Handle a fifteen-year-old legacy system with no APIs" is exactly as unmeasurable until you ask the analogous questions: what data-access surfaces actually exist (direct database reads, nightly file exports, screen automation), who owns changes to that system, what staleness is tolerable in whatever gets extracted, and what happens to your system if that legacy system goes down for a day. "No APIs" becomes a concrete integration contract the same way "handle spikes" becomes a concrete capacity contract, by naming the constraint that changes the design instead of accepting the vague label.
Worked example: turning "5x for ten minutes" into a server count
Assume measured baseline steady-state traffic of 1,000 requests per second (RPS), and product says the spike is "5x for about ten minutes." Assume each server instance safely handles 200 RPS at target latency:
baseline servers=2001,000=5 spike RPS=5×1,000=5,000 spike servers needed=2005,000=25Now check whether autoscaling can even react in time. Assume it takes 3 minutes from scale-out trigger to a new instance serving traffic:
spike duration (10 min)>scale-out reaction time (3 min)Autoscaling alone is workable here, with roughly 3 minutes of degraded capacity at the start of the spike. If the same 5x spike instead lasted 60 seconds (a flash-crowd shape rather than a sustained one), the 3-minute scale-out reaction time would exceed the entire spike duration, and the only real fix is pre-warmed standby capacity, not faster autoscaling. That is why duration and arrival shape change the design, not just the multiplier.
Trade-offs & pitfalls
- Pitfall: designing for "handle any spike" instead of a bounded one. Every system has a ceiling; the point of these questions is choosing it deliberately instead of discovering it during an incident.
- Pitfall: assuming autoscaling reaction time is negligible. If it is not faster than the spike itself, pre-provisioned headroom is needed, which costs money sitting idle.
- Graceful degradation (returning cached or partial results, shedding low-priority requests) is usually cheaper than provisioning for the absolute peak, but only if product has said which features are allowed to degrade.
You have a fixed monthly hosting budget and a requirement to keep p95 API latency under 200ms. Walk through how you'd quantify the trade-off between spending more to improve latency and staying within budget.
Sample Answer
Direct answer
Treat this as finding the cheapest lever that gets the 95th-percentile (P95) latency under 200 ms, not as a single yes-or-no spend decision: price out each candidate lever (a bigger cache, more instances, a faster database tier) in dollars per millisecond improved, then take the cheapest ones first until the target is hit or the budget runs out.
Structured elaboration
Levers and how to price them
Build a small table of candidate levers, each with its cost delta and its expected latency delta, measured or estimated from a canary or A/B test, then rank by dollars per millisecond and fund down the list until the 200 ms target is met or the budget is spent. Whichever lever is left over is what you tell the budget owner you couldn't afford.
Worked example: the absorbed cache lever
Assume 1,000 requests per second (RPS), a current cache hit rate of 70% (so 30% of requests, 300 RPS, reach the origin), and each origin instance safely handles 20 RPS at the target latency, with 1.5x headroom for safety margin:
origin RPS at 70% hit rate=0.30×1,000=300 instances needed=⌈20300×1.5⌉=23Raising the hit rate to 90% (bigger cache, longer time-to-live) drops origin load to:
origin RPS at 90% hit rate=0.10×1,000=100 instances needed=⌈20100×1.5⌉=8At an illustrative $0.10 per instance-hour (730 hours/month):
cost at 23 instances=23×730×0.10=$1,679/month cost at 8 instances=8×730×0.10=$584/month savings=1,679−584=$1,095/month (≈65%)The cost being traded here is staleness: a longer time-to-live bounds how out of date a cached response can be, so the real trade is "up to N seconds of staleness" versus "$1,095/month of origin capacity," not latency versus cost in the abstract.
Comparing against a direct lever
If instead a database-tier upgrade costs an illustrative extra $4,000/month and takes P95 from 260 ms to 190 ms:
dollars per ms=260−1904,000≈$57.14/msCompare that figure against the cache lever's effective dollars-per-ms and take whichever is cheaper first; reach for the database upgrade only once the cheaper levers are exhausted and the target is still not met.
Trade-offs & pitfalls
- Pitfall: comparing levers by their sticker cost instead of their cost per millisecond improved; a cheap lever that barely moves P95 can be worse value than an expensive one that clears the whole target.
- Caching trades latency and cost for staleness, not for nothing; a time-to-live long enough to matter for cost has to be checked against what the product can tolerate seeing stale.
- Pitfall: optimizing average latency instead of P95; a lever that helps the median can leave the tail, and the service-level objective (SLO) you're actually measured on, untouched.
- Validate every lever with a real canary or A/B test before committing budget; the arithmetic above is a planning estimate, not a substitute for measuring it.
Explain the difference between latency and throughput, and how the two relate to each other.
Sample Answer
Direct answer
Latency is how long a single request takes from request to response; throughput is how many requests the system completes per unit time. They are related through concurrency: at a fixed level of concurrency, throughput is roughly concurrency divided by latency, so throughput can be raised either by lowering per-request latency or by running more requests concurrently, at least until the system runs out of capacity, at which point requests start queueing and both latency and its variance rise sharply.
Structured elaboration
Little's Law is the bridge between the two
L=λW
where L is the average number of requests in the system (concurrency), lambda is the arrival rate (throughput), and W is the average time each request spends in the system (latency). This one relationship connects the two metrics completely.
Two regimes
- Below capacity: adding concurrency raises throughput roughly linearly without raising latency much, since the extra work overlaps with idle capacity.
- Near or above capacity: requests start queueing behind each other, and latency rises non-linearly. A small increase in load causes a disproportionate jump in the tail, a pattern basic queueing models (for example M/M/1: the standard textbook queueing model for one server with random arrivals and random service times, the model that produces the classic curve where wait time explodes as utilization approaches 100%, named here, not derived) predict and production systems reliably show.
Why percentiles, not the average, matter once load is near capacity
The mean can look fine while a growing minority of requests wait behind a queue; the 95th and 99th percentile (P95/P99) surface exactly what the average hides.
The two are not always aligned
Batching, processing many items in one call to raise throughput, typically raises the latency of any individual item in the batch. A system tuned to maximize throughput at all costs (large batches, very high concurrency) can make its own P99 latency worse, which is why the metric worth optimizing depends on the workload: a public, user-facing API should optimize for tail latency at a given throughput target; an overnight batch job should optimize for total throughput and mostly ignore any single item's latency.
Worked example
Suppose a service's actual processing time per request (its service time) is 10 ms, and the target is 500 requests/sec sustained. Little's Law says the average number of requests being served concurrently at that point is:
L=λ×Wservice=500 req/s×0.01 s=5 concurrent requests
If the worker pool has exactly 5 workers, the system is running at 100% utilization, and queueing theory's core warning applies: at or near full utilization, queue length and wait time become highly unstable, since there is no slack to absorb any variance in arrival timing or request duration. Sizing to a target utilization of about 75% instead:
capacity=ρtargetL=0.755≈6.7⇒7 workers
gives the system headroom to absorb bursts without its tail latency exploding, at the cost of running roughly 40% more capacity than the bare-minimum number, capacity that sits partly idle most of the time. That headroom is not waste; it is the price of a stable P99.
Beyond the mechanics, defending a capacity decision to non-engineering stakeholders usually means presenting this same relationship visually: a P50/P95/P99 latency trend next to a throughput trend over the same time window, so a viewer can see the point where rising throughput starts dragging tail latency up, rather than being told about it in the abstract.
Trade-offs & pitfalls
- Quoting only an average latency number, which hides that the system may already be close to its queueing knee for a meaningful fraction of requests.
- Treating "increase throughput" and "decrease latency" as the same goal; batching and running near full utilization both raise throughput while making individual-request latency worse.
- Sizing capacity to exactly the average expected load instead of leaving headroom, which looks efficient on a spreadsheet and causes a tail-latency incident on the first genuinely busy day.
- What a senior answer adds: naming which of the two metrics the workload actually cares about, rather than reciting the definitions of latency and throughput and stopping there.
An order processing flow includes payment authorization, an inventory check, and a fulfillment job. Would you build this as one synchronous API call the client waits on, or break it into asynchronous steps? Walk through what you gain and what you give up with each choice.
Sample Answer
Direct answer
For a flow like payment authorization, an inventory check, and a fulfillment job, keep only the step whose result the customer must see before checkout can honestly end synchronous, which is usually just payment authorization, and push the rest onto an asynchronous, queue-backed path. A single synchronous call across all three steps is simpler to write and reason about, but it ties the customer-facing latency and thread capacity of the whole checkout to whichever downstream step is slowest, which in practice is fulfillment, not payment.
Structured elaboration
Little's Law is the lens: for a fixed pool of request-handling threads, throughput is roughly the pool size divided by how long each request holds a thread.
Throughputmax≈WholdC
Anything that lengthens the hold time, such as a slow downstream call held inside a synchronous request, directly divides the throughput ceiling.
What synchronous gives you
- A simpler failure model: one request either fully succeeds or the client gets an immediate, unambiguous error.
- No separate reconciliation step: ordering across the three steps is enforced just by the fact that they run one after another in the same call.
What synchronous costs you
- Customer-facing latency is the sum of all three steps' latencies.
- A slowdown anywhere downstream, even in a step unrelated to payment, shows up to the customer as a checkout timeout.
- The thread or connection pool sized for "handle a checkout" is now implicitly sized for "handle the slowest thing checkout touches."
What asynchronous gives you
- The customer-facing call only does the one step that must be known before you can honestly say "your order is placed."
- Inventory check and fulfillment run off a queue with their own retry and backoff, without holding a checkout thread while they run.
- Throughput scales with the fast, customer-facing step, not the slow, background one.
What asynchronous costs you
- A way to communicate status back to the user across states (accepted, confirmed, shipped), instead of one final answer.
- Retry and dead-letter handling (a separate holding queue for messages that failed every retry attempt, so they can be inspected instead of silently vanishing) for the steps that now run out-of-band.
- A compensating action (a refund, a backorder notice) for the case where a later async step fails after the customer was already told "order placed," a case synchronous designs never face because they never say yes before every step is known good.
One layer down: orchestration vs. choreography
The same judgment reappears in how the asynchronous steps themselves are coordinated: an orchestrator explicitly sequences payment, then inventory, then fulfillment from one coordinating service, which makes it easy to answer "where is order 42 right now." Choreography lets each service react to events the others emit, with no central coordinator, which is more decoupled but harder to trace. A status dashboard that aggregates order state across services needs to know which pattern is in play, since orchestration gives it one authoritative place to poll and choreography means reconstructing state from a stream of events instead.
Worked example
Assume a pool of 200 request-handling threads, one thread held per in-flight request (a common thread-per-request model).
Synchronous design. Combined hold time across payment authorization (about 300 ms), the inventory check (about 200 ms), and enqueuing the fulfillment job (about 300 ms) is 0.8 s:
Throughputsync≈0.8 s200 threads=250 req/s
If the fulfillment step's downstream system (say, a warehouse system under its own load) slows to 5 s, unrelated to payment or inventory, the combined hold time becomes payment (300 ms) plus inventory (200 ms) plus the now-slow fulfillment step (5,000 ms), or 5.5 s total, so the same 200 threads now yield:
Throughputsync,degraded≈5.5 s200≈36.4 req/s
a 250/36.4 \approx 6.9x drop in checkout throughput caused entirely by a system that has nothing to do with authorizing the customer's payment.
Asynchronous design. Only payment authorization is held synchronously (about 300 ms); inventory check and fulfillment are handed to a queue immediately after.
Throughputasync≈0.3 s200≈667 req/s
about 2.7x the synchronous baseline, and it stays there even if fulfillment slows down, because a slow queue backs up (visible as growing queue depth, an operational signal to alert on) instead of stealing checkout capacity.
Trade-offs & pitfalls
- Making everything async "for scalability," including the one step where the customer genuinely needs an authoritative yes or no before the interaction can end; payment authorization itself should not become eventually consistent from the customer's point of view.
- Adding async processing without adding the status visibility and compensating actions it requires, turning a downstream failure into a silently stuck order.
- Sizing the thread or connection pool for the fast steps and being surprised when a slow, unrelated dependency exhausts it, exactly the arithmetic shown above.
- What separates a senior answer: naming the single step that genuinely must complete before responding to the customer, rather than treating "sync" and "async" as an all-or-nothing choice for the whole flow.
flowchart TB
subgraph SYNC["Synchronous: one blocking call"]
direction TB
C1[Client checkout request] --> P1[Payment authorization]
P1 --> I1[Inventory check]
I1 --> F1[Fulfillment job]
F1 --> R1[Response to client]
end
subgraph ASYNC["Asynchronous: fast accept, rest on a queue"]
direction TB
C2[Client checkout request] --> P2[Payment authorization]
P2 --> R2[Order accepted response]
P2 --> Q1[Queue: inventory check plus fulfillment]
Q1 --> ST1[Status updates to client]
end
Unlock Full Question Bank
Get access to all System Design Methodology and Trade-off Analysis interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.