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.
You need storage for two different use cases in the same system: a low-latency user-session store with automatic expiry, and an append-only audit log. Would you use the same datastore for both, or different ones? Walk through your reasoning.
Sample Answer
Direct answer
Use two different datastores, because the two workloads sit at opposite ends of nearly every axis that matters: the session store needs low-latency point lookups by key with automatic expiry, while the audit log needs durable, ordered, append-only writes with long retention and replay. Forcing one engine to serve both means either paying the audit log's durability and retention cost on every fast session read, or giving up the audit log's append-only, replayable guarantees.
Structured elaboration
| Dimension | Session store | Audit log |
|---|---|---|
| Access pattern | Point lookup and update by key | Append-only write, range/time-based scan |
| Expiry | Automatic (time-to-live, TTL) | None, typically long or indefinite retention |
| Latency need | Very low, on the request's critical path | Can tolerate higher write latency, rarely on the user-facing critical path |
| Durability | Often acceptable to lose on crash (session can be re-created) | Must be durable, it's the record of what happened |
| Natural fit | Redis (an in-memory key-value store) with native TTL | An append-only log or stream (for example, Kafka, a distributed log/streaming platform), or a partitioned relational table |
Why this generalizes beyond sessions and audit logs: the absorbed framing of a machine-learning (ML) metadata store shows the same underlying shape. Continuous integration (CI) pipelines generate a high write rate logging experiment runs, while dashboards and monitoring run read-heavy, scan-heavy queries over historical runs. That's structurally the identical pattern, a write-optimized append path pulling against a read-optimized query path, which is why the "one store or two" question should be re-asked per component rather than defaulting to whatever store is already in the stack.
Worked example
Primary scenario (session + audit). Use Redis for sessions, with native per-key TTL handling expiry automatically, no cleanup job required. Use an append-only log or a partitioned table for the audit trail.
Storage growth check on the audit side: assume, illustratively, 500 audit events per second at 1 KB each.
500 events/s×1KB=500KB/s
500KB/s×86,400s/day=43,200,000KB/day=43.2GB/day (decimal GB, 1 GB = 1,000,000 KB)
43.2GB/day×730days (2 years)=31,536GB≈31.5TB
Thirty-one and a half terabytes of append-only history is exactly the kind of cheap, sequential, long-retention storage a log-oriented system is built for, and exactly the kind of volume that would be prohibitively expensive to hold in an in-memory session store's provisioned capacity.
Second scenario (ML metadata, the absorbed framing). The same reasoning applies with a relational or document store for structured experiment metadata (read-heavy, queried by dashboards across many attributes) paired with an append-only log or object storage for the raw high-volume run logs written continuously by CI, again two engines, chosen for two genuinely different access patterns, not one engine stretched across both.
Trade-offs & pitfalls
- Storing audit events in the session store "for convenience" either evicts old sessions to make room or silently drops the durability guarantee the audit log actually needs, neither is acceptable for an audit trail.
- Using an append-only log system for session storage means reimplementing per-key expiry logic that a key-value store like Redis provides natively, most log/stream systems have topic-level retention, not per-key TTL.
- Two datastores are two systems to operate, monitor, and back up, that overhead is worth paying here because the access patterns are genuinely incompatible; it would not be worth it if the two workloads were similar enough to share one engine.
- Deletion requirements, for example a regulatory "right to be forgotten" request, are harder to satisfy in an append-only log than in a key-value store, this needs a tombstone (a marker record saying "treat this key as deleted") plus a compaction strategy (the background process that rewrites and shrinks the log over time, actually purging tombstoned data) decided up front, not retrofitted after the first request arrives.
flowchart LR
Client --> SessionAPI[Session Read/Write]
Client --> AuditWriter[Audit Event Writer]
SessionAPI --> Redis[(Redis Session Store)]
AuditWriter --> Log[(Append-only Log)]
Log --> ColdStorage[(Long-term Storage)]
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.
What's the difference between a high-level architecture (system context and major components) and a component-level design (interfaces, data flows, sequencing)? What would you actually show stakeholders at each level, and what's one decision that only makes sense at the high level?
Sample Answer
Direct answer
A high-level architecture shows the system's scope: the major building blocks (client, API layer, service tier, datastore, cache, external dependencies), how they relate, and the non-functional constraints (scale, availability) that shaped them. A component-level design zooms into one of those blocks and specifies its interfaces, request/response schemas, data flows, and sequencing. You show the high-level view to stakeholders who need to understand what the system is and what it costs or risks; you show component-level design to the people who have to build, test, or integrate against one specific piece.
Structured elaboration
| Dimension | High-level architecture | Component-level design |
|---|---|---|
| Purpose | Scope, responsibilities, external actors, major blocks, non-functional constraints | Internals of one component: interfaces, data formats, control flow, error paths, sequencing |
| Typical diagrams | System context diagram, high-level component diagram, deployment diagram (regions, load balancers, replicas) | Sequence diagram for a specific flow, API contract (request/response schema), data model / entity-relationship diagram |
| Audience | Product managers, other architects, executives, site reliability engineers (SRE), business stakeholders | Backend/frontend engineers, QA, API consumers, integration partners |
| Question it answers | "What is this system, and what are its risk and cost boundaries?" | "How exactly does this one feature work end to end?" |
| Example decision that only lives here | Monolith vs microservices for the whole platform (changes team structure, operational model, and cost) | The exact endpoint shape, schema, and authentication header format for one API |
The reason both layers matter: the high-level view sets the strategy and the constraints everyone else has to work inside; the component-level view is what actually gets implemented, tested, and integrated. A good design doc keeps an explicit mapping from each high-level block down to its component-level detail, so a reviewer can move between the two without re-deriving context.
Worked example
Say you're designing a subscription billing feature. At the high level you'd draw: client apps, an API gateway, a billing service, a payments component, a database, and a message queue for async notifications, with an arrow showing the billing service calls out to a third-party payment processor. The one decision that belongs only at this level: whether billing lives inside the existing monolith or is split into its own service, because that choice affects deployment, on-call ownership, and the blast radius of an incident, not just this one feature.
At the component level, you'd zoom into just the billing service and produce: a sequence diagram for "create subscription" (client → billing service → payments component → processor → database write → event published), the exact request/response schema for the POST /subscriptions endpoint, and an entity-relationship diagram for the subscription and invoice tables. None of that detail belongs on the high-level diagram; it would bury the one decision (monolith vs separate service) that the high-level view exists to surface.
Trade-offs & pitfalls
- Showing component-level detail (full schemas, every retry path) to an executive or product stakeholder buries the one decision they actually need to weigh in on.
- Skipping the high-level view and jumping straight to component design risks locking in a boundary (a shared database, a synchronous call where an event would do) that is expensive to undo later, because it was never surfaced as a decision.
- A common weak answer just says "high-level is the big picture, low-level is the details" without naming a decision that is exclusive to one level; naming that decision is the signal an interviewer is listening for.
- Keep a living link between the two artifacts (a component-level design should reference which high-level block it belongs to) so the documentation doesn't drift apart as the system evolves.
A proposed optimization would cut your service's tail latency in half, but it would triple infrastructure cost and add real deployment complexity. How do you decide whether it's worth shipping, and what would change your answer?
Sample Answer
Direct answer
Treat it as an investment decision: translate the latency improvement into a dollar figure, usually via conversion or engagement lift, compare it to the extra cost plus the risk the added complexity introduces, and ship it only if the net benefit is positive with a margin that survives your uncertainty about the conversion assumption. What changes the answer is the size of that margin: a break-even case should prompt more evidence (a real experiment) before committing, not a coin flip.
Structured elaboration
The decision framework
- Translate tail latency into revenue using your own historical relationship between latency and conversion, not an assumption invented for this decision alone.
- Compare that revenue gain to the extra infrastructure cost plus the risk-adjusted cost of the added complexity (a higher chance and cost of an incident, a longer time to diagnose an outage).
- Validate with a phased rollout (a small canary first) before committing the full 3x spend, so the confirmed number is what's being paid for, not the projected one.
- Set a rollback trigger up front: if the reliability cost shows up (more incidents, longer mean time to recover) before the revenue gain is confirmed, pull back.
Worked example (illustrative assumptions; would come from your own A/B data in practice)
Assume current infrastructure costs $50,000/month and the proposed change triples it to $150,000/month:
extra cost=150,000−50,000=$100,000/monthAssume 99th-percentile (P99) tail latency drops from 2,000 ms to 1,000 ms, a 1,000 ms reduction, and assume (illustrative, pulled from historical experiments in a real decision) a 0.5% relative conversion lift per 100 ms of P99 reduction:
relative conversion lift=1001,000×0.5%=5%Against baseline monthly revenue of $2,000,000:
revenue gain=2,000,000×0.05=$100,000/month net benefit=100,000−100,000=$0That is a break-even case at these assumptions, exactly the situation that should prompt a real experiment (canary the change to a fraction of traffic, measure the actual conversion delta) rather than a decision made purely on the spreadsheet. If the elasticity assumption were even slightly optimistic, this ships negative.
What would change the answer
- A larger baseline revenue (the same 5% lift is worth more on a bigger base) tips it positive without changing anything else.
- A confirmed, measured elasticity from a canary experiment replaces the illustrative 0.5% per 100 ms figure with a real one.
- A materially lower or higher risk-adjusted reliability cost (the 3x infrastructure is also more than 3x more complex to operate) shifts the true cost side of the equation.
- A non-revenue reason, such as a contractual service-level agreement (SLA) or a strategic customer who explicitly asked for this, can justify shipping even at a break-even or slightly negative revenue case.
Trade-offs & pitfalls
- Pitfall: treating the latency-to-revenue elasticity as a known constant instead of an assumption to validate; shipping a 3x cost change on an invented number is the actual failure mode this question is testing for.
- Pitfall: ignoring the complexity side of the cost. Three times the infrastructure is usually more than three times the operational surface area (more failure modes, longer incident diagnosis), and that risk has a cost even when nothing has broken yet.
- A break-even or narrowly-positive case is a signal to run a smaller, reversible experiment, not to commit fully in either direction.
- Don't ignore alternatives: a targeted optimization for only the highest-value request paths, or a hybrid where only certain traffic gets the expensive treatment, sometimes captures most of the benefit at a fraction of the cost.
You're building a stateful, write-heavy service that needs to sustain 10,000 writes per second with low latency. How does that write-heavy profile change your datastore and architecture choices compared to a read-heavy service?
Sample Answer
Direct answer
A sustained 10,000 writes-per-second, low-latency, stateful workload pushes you away from a design tuned for reads (a single write primary, heavy indexing, read replicas) and toward one built for write scaling: a storage engine optimized for sequential writes, a partitioning scheme that spreads writes across many nodes, and a replication model with an explicit, tunable durability-versus-latency trade-off rather than a single write bottleneck.
Structured elaboration
Why a read-optimized design breaks down here. Traditional B-tree storage engines perform random-access writes and update every index on every insert, each additional index roughly adds another write per record. A single-writer relational primary caps total write throughput at whatever one node's disk and CPU can sustain, and read replicas do nothing for write capacity, they only copy the primary's write stream.
What changes for write-heavy:
- Storage engine: log-structured merge (LSM) tree engines (used by databases like Cassandra, HBase, and the storage layer behind DynamoDB-style stores) append writes sequentially and merge them in the background, trading some read amplification (a single logical read may have to check several separate on-disk files before it can answer, since recent and older writes land in different segments) for much higher sustained write throughput than a B-tree.
- Partitioning: writes are sharded across many nodes by a partition key. The key must be chosen for even cardinality, a monotonically increasing key (like a timestamp or auto-increment ID) concentrates all new writes on one shard regardless of how many nodes exist.
- Replication and durability: instead of one primary with no built-in fan-out, use a quorum-based replication scheme, writes are acknowledged once a majority of replicas confirm, giving a tunable point between "acknowledge on one node" (fast, risks data loss) and "acknowledge on all nodes" (safest, slowest).
- Indexing discipline: keep secondary indexes to the minimum the write path can afford, every index is a write, this is the opposite instinct from a read-heavy design where more indexes are usually free wins.
Worked example
Assume, illustratively, that a single write-optimized node sustains 2,000 writes per second at the target latency.
Nodes needed for raw throughput: 10,000/2,000=5 shards.
For durability, replicate each shard three ways (tolerate one node failure without data loss): 5×3=15 total storage nodes.
A quorum write with N=3 replicas and a write quorum of W=2 means the client waits only for the second-fastest replica to acknowledge, not the slowest, bounding tail write latency while still guaranteeing the write survives a single node failure.
Cost contrast, provisioned versus per-operation pricing. At an illustrative $0.00001 per write operation under a consumption-priced managed service:
ops/day=10,000×86,400=864,000,000 writes/day
daily cost=864,000,000×$0.00001=$8,640/day≈$259,200/month
Against 15 provisioned nodes at an illustrative $400/node/month: 15×$400=$6,000/month. At this sustained write rate the per-operation model costs roughly 40 times more, which is why sustained high-volume writes usually favor provisioned or self-managed clusters, and why consumption pricing fits bursty, low-average workloads instead.
Trade-offs & pitfalls
- Carrying over every index from a read-heavy design roughly multiplies write cost by the number of indexes, audit which indexes the write path can actually afford.
- A low-cardinality or monotonically increasing partition key creates a hot shard that caps total throughput no matter how many nodes you add, this is the single most common write-scaling mistake.
- Waiting for all replicas (W=N) is the safest durability setting but the slowest; a majority quorum balances safety and latency, the exact quorum size is itself a trade-off decision, not a default.
- High write concurrency needs connection pooling and write batching, naive one-connection-per-request patterns hit connection limits long before they hit the storage engine's real capacity.
flowchart LR
Client --> Router[Write Router]
Router --> ShardA[Shard A Leader]
Router --> ShardB[Shard B Leader]
Router --> ShardC[Shard C Leader]
ShardA --> ShardARep[Shard A Replicas x2]
ShardB --> ShardBRep[Shard B Replicas x2]
ShardC --> ShardCRep[Shard C Replicas x2]
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.