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 a managed database service against running your own self-managed database cluster for a high-throughput OLTP workload. What cost categories, operational trade-offs, and reliability differences would you weigh?
Sample Answer
Direct answer
Compare them on three axes, cost, operations, and reliability, and expect labor cost to dominate the comparison more than raw infrastructure price: a managed service usually costs more per instance-hour but removes most of the patching, backup, and failover work that a self-managed cluster needs a dedicated person to own, which is often the bigger number.
Structured elaboration
Comparison table
| Category | Managed database | Self-managed cluster |
|---|---|---|
| Compute/storage cost | Higher per instance-hour (built-in overhead for the service) | Lower per instance-hour, but you provision it yourself |
| Operational labor | Near-zero incremental; the provider handles patching, backup, failover | Needs dedicated database administration or site-reliability time |
| Reliability/availability | Automatic failover, tested replication, published availability target | You design and test failover yourself; only as reliable as your own runbooks |
| Scaling | Usually a configuration change or a supported read-replica pattern | You build and validate the scaling path yourself |
| Control/customization | Limited to what the provider exposes | Full control over engine version, extensions, tuning |
| Lock-in | Higher if you use provider-specific features | Lower; more portable across environments |
When each is the right call
Managed fits when the team has limited dedicated database or site-reliability engineering headcount, when the online transaction processing (OLTP) workload needs a strict, well-tested availability target quickly, or when the provider's built-in scaling features fit the workload's actual bottleneck. Self-managed is justified when the workload needs an engine feature or extension the managed offering doesn't expose, when the scale is large enough that infrastructure plus automation genuinely beats managed pricing, or when a regulatory requirement demands direct control over maintenance windows, key handling, or backup policy that a managed service won't let you set yourself.
Worked example: where the real cost difference comes from
Assume a 3-node OLTP cluster (one primary, two replicas), illustrative rates: self-managed compute at $0.40 per instance-hour, managed-service compute at $0.55 per instance-hour (a 37.5% premium for the service), storage and backup roughly equal at $200/month either way, 730 hours/month.
self-managed compute=3×730×0.40=$876/month managed compute=3×730×0.55=$1,204.50/monthNow add labor. Assume self-managed needs 0.3 full-time-equivalent (FTE) of database or site-reliability time for patching, backup verification, and failover testing, at a fully-loaded cost of $150,000/year, or $12,500/month per FTE:
self-managed labor=0.3×12,500=$3,750/monthManaged needs only an assumed 0.05 FTE for configuration and monitoring:
managed labor=0.05×12,500=$625/monthTotal monthly cost:
self-managed total=876+200+3,750=$4,826/month managed total=1,204.50+200+625=$2,029.50/monthAt this illustrative scale, the managed option is actually cheaper overall despite its higher unit price, because labor dominates the total. That inverts once the cluster is large enough that the managed premium's absolute dollar gap exceeds what 0.3 FTE of labor costs, which is the "very large scale" condition under which self-managed becomes justified on cost.
Trade-offs & pitfalls
- Pitfall: comparing only instance-hour pricing and concluding self-managed is always cheaper; labor is the number that most often flips the comparison.
- Migration complexity (schema quirks, extension dependencies, connection-handling differences) is a real, often underestimated cost on either side of a switch.
- Hidden managed-service costs to watch for: input/output charges, cross-region data transfer, and support-tier pricing that isn't in the sticker instance price.
- Enterprise support contracts and published service-level agreements (SLAs) on either side change the reliability comparison; a self-managed cluster's reliability is only as good as the runbooks and testing actually behind it.
Walk me through a back-of-envelope monthly cost estimate for a simple web app expected to handle 1,000,000 requests per day and 10 TB of outbound data per month. What assumptions do you state, and what do you sanity-check at the end?
Sample Answer
Direct answer
Break the estimate into three buckets, compute, storage, and network egress, state a small number of explicit assumptions for each (traffic shape, cache hit ratio, unit prices), and multiply through. For this workload, 1,000,000 requests/day and 10 TB of outbound data/month, the arithmetic below lands around 1,246 dollars/month using illustrative unit rates, with network egress the dominant line item. The one sanity check worth doing at the end is dividing total outbound data by total requests: it implies each request carries roughly 333 KB on average, which is large for a "simple web app" and should prompt asking whether the two given numbers actually describe the same traffic.
Structured elaboration
Why three buckets, always kept separate
Compute, storage, and network egress scale with different things (request rate, data volume at rest, data volume transferred), so lumping them together hides which one actually drives the bill. For a workload described mainly by a request count and an outbound-data figure, network egress is very often the surprise line item, since it scales with bytes moved, not with request count.
Stated assumptions (illustrative unit rates, not any specific vendor's current list price, so the arithmetic below is fully reproducible from these inputs alone):
- 1 TB = 1,000 GB for this estimate (a decimal convention, kept simple and stated once; real billing sometimes uses the binary definition instead).
- A 5x peak-to-average traffic ratio, a common assumption absent a stated diurnal profile.
- Compute: $0.10 per instance-hour; a minimum of 3 instances regardless of load, for basic redundancy and zero-downtime deploys.
- Storage: a flat $20/month (small, since a "simple web app" is not primarily a database- and storage-heavy workload).
- Network: 60% of requests served from a content delivery network (CDN) cache; origin egress (cache misses only) at $0.09/GB; CDN edge egress (all bytes delivered to users) at $0.06/GB.
- Miscellaneous (load balancer, monitoring, DNS): $50/month.
Worked example
Compute.
avg RPS (requests per second)=86,4001,000,000≈11.6,peak RPS≈11.6×5≈58
Even one modest instance clears 58 req/s comfortably for a typical stateless web app, so the instance count here is driven by redundancy, not raw capacity: 3 instances.
compute=3×$0.10/hr×24×30=$216/month
Network egress, the dominant cost for this workload. Two separate legs, both real: the cache-miss traffic the CDN pulls from the origin (pricier), and the full volume the CDN delivers to end users regardless of hit or miss (cheaper, volume-discounted).
miss traffic=10,000 GB×(1−0.6)=4,000 GB
origin cost=4,000 GB×$0.09/GB=$360
CDN cost (all delivered bytes)=10,000 GB×$0.06/GB=$600
Total.
total≈$216+$20+$360+$600+$50=$1,246/month
Sanity check, the part the question explicitly asks for. Cross-check the two given numbers against each other, not just against the chosen unit prices:
requests/month=1,000,000×30=30,000,000
avg payload=30,000,00010,000 GB×1000 MB/GB≈0.33 MB≈333 KB per request
A typical JSON API response is a few KB, not a third of a megabyte. A 333 KB average suggests this "simple web app" is actually serving images, downloads, or media, not just API calls, or that the two input numbers don't describe the same traffic, for instance if the 10 TB includes a batch export job outside the 1,000,000 daily request count. That is the real value of this sanity check: it isn't re-verifying the arithmetic, it's confronting whether the two numbers handed to you are internally consistent with the story you were told, before handing a stakeholder a dollar figure built on an unstated contradiction.
Trade-offs & pitfalls
- Pricing only one leg of egress (origin-to-CDN or CDN-to-user) and treating the network line item as done.
- Skipping the sanity check and presenting the total as precise when the underlying assumptions (cache hit ratio, peak ratio) were guesses; state which input the total is most sensitive to.
- Sizing compute purely off average load; for small-to-medium workloads, redundancy and deploy safety often set the compute line, not raw throughput math.
- What separates a senior answer: showing the arithmetic and stating which two given numbers were cross-checked at the end, and why, rather than presenting a total as if it fell out of a spreadsheet with no further scrutiny.
A social feed needs replies to always appear after their parent post, but the rest of the feed's propagation can be eventually consistent. How would you design the consistency model for this one feature, and what would you measure to confirm ordering and staleness stay within an acceptable range?
Sample Answer
Direct answer
Give the reply-to-parent relationship a strong, causal ordering guarantee (a reply is never visible before the post it replies to), while letting everything else in the feed (impression counts, non-causal fan-out, feed ranking) settle down eventually. Achieve the causal guarantee without a full linearizable write path by attaching a compact causal timestamp to each write and holding a reply back at the read side until its parent is locally visible. Confirm it worked by measuring the ordering-violation rate directly (aim for effectively zero) and the write-to-visibility staleness distribution, not by trusting the design on paper.
Structured elaboration
Framing the guarantee
This is a per-feature consistency decision, not a system-wide one: only the parent-to-reply edge needs an ordering guarantee, so only that edge should pay for it. Making the whole feed strongly consistent to protect one relationship is the wrong instinct.
Mechanism: causal delivery, not full consistency
- Tag every write with a hybrid logical clock (HLC) timestamp, a logical clock that mixes physical time with a counter so causally related events can be ordered without comparing a full history per item.
- A reply carries a dependency: (parent_id, parent_HLC).
- At the delivery point in each region, hold a reply in a small buffer until its parent is present locally, showing a short "loading" state if needed, then release it. This gets you causal consistency (a specific point between strict linearizability, where every operation appears to happen instantly in one single global order that every reader sees the same way, and plain eventual consistency, which gives no ordering guarantee at all and just "settles" eventually) for exactly the edge that needs it.
- A full vector clock (one counter per replica) would also give causal ordering, but at a metadata cost that grows with the number of regions; HLC stays a fixed few bytes regardless of region count, which is why it is the better default at this scale (see the worked example).
The absorbed risk: this is not the same anomaly as a shopping-cart race
The reply-ordering problem is a single-writer, append-only causality problem: one thing must come after another. It should not be confused with the concurrent-write anomalies a feature like a shopping cart's quantity field runs into: lost updates (two concurrent writes to the same field, one silently overwrites the other) and write skew (two transactions each read a consistent snapshot, both commit valid-looking changes that are jointly invalid). Those need a different tool, optimistic concurrency control (a version check on write) or serializable transactions, because the problem is conflicting writes to the same value, not the order two different items become visible. Naming both anomaly families shows you are choosing the guarantee to fit the failure mode, not applying one mechanism everywhere.
Decision criteria to reuse elsewhere in the feed
Ask, for each field or edge: does a violation change what a user believes happened (a causality break) or does it just show slightly stale aggregate state (a like count)? Is the anomaly a mis-ordering (needs causal or session guarantees) or a lost or conflicting write (needs concurrency control)? What is the cost of buffering or retrying versus the cost of a global synchronous commit?
Worked example
Assume a mid-size product doing 5,000,000 replies per day globally, replicated across 6 regions.
Metadata overhead, HLC versus a full version vector (worst case, one entry per region that has touched the thread):
HLC size≈12 bytes (fixed) Version vector size≈6×12 bytes=72 bytes (worst case) Extra bytes/day=5,000,000×(72−12)=300,000,000 bytes≈286 MiB/dayThat gap is why HLC is the better default once you are geo-replicating at this volume: the version vector's cost scales with region count, HLC's does not.
Now size the read-side buffer. Assume cross-region replication lag has a 99th-percentile (P99) of 300 ms (illustrative; in practice you would pull this from your own replication metrics, not assume it):
buffer timeout=300 ms⇒covers≥99% of replies without a remote fetch fallback rate≈1%×5,000,000=50,000 reads/day≈0.58 reads/sec averageFifty thousand read-repair fallbacks a day is cheap; that number is what tells you a 300 ms buffer is a reasonable default rather than a guess.
Trade-offs & pitfalls
- Buffer timeout is the main dial: too short and you push load onto cross-region read-repair; too long and users occasionally see a stale placeholder instead of the reply.
- Hot threads (a viral post with a reply storm) can blow past the default buffer's assumptions; the fix is to synchronously fast-path-replicate just the parent for hot threads, not to raise the global timeout for everyone.
- Pitfall: conflating "causal ordering for one edge" with "the whole feed is strongly consistent." Interviewers listen for whether you can name the narrow guarantee you actually need.
- Pitfall: monitoring only the design's intent ("we use causal delivery") instead of the outcome (measured violation rate, measured staleness). A design without those two numbers wired up is unverified.
- Garbage-collect dependency-tracking state once a reply has been durably applied everywhere you care about; unbounded metadata retention is a quiet cost leak.
A client tells you: 'our web application must feel fast for users worldwide.' How would you translate that into concrete, measurable non-functional requirements?
Sample Answer
Direct answer
Translate "feels fast" into measurable, percentile-based service-level objectives (SLOs, the internal targets a team designs to) broken out by user geography and device class, because a single global average latency number hides the users who are actually having a bad experience. Concretely: pick a small set of user-perceived timing metrics, set targets for the 95th and 99th percentile (P95/P99), not just the median, and set different targets per region, since physics, not engineering effort, sets a latency floor for users far from the servers.
Structured elaboration
Why percentiles, not averages
The median (P50) reflects the typical user; P95 and P99 reflect the users who are actually complaining, and those are the ones a business should worry about losing.
Candidate user-perceived metrics (standard web-performance terms, named here without inventing a universal target for each, since the right target is a product decision):
- Time to First Byte (TTFB): how long until the server starts responding.
- First Contentful Paint (FCP): how long until something appears on screen.
- Time to Interactive (TTI): how long until the page actually responds to input.
Segmentation
- By region: a request served from a single origin has a very different latency floor depending on how far the user is from that origin (worked example below).
- By device and network class: a phone on a mobile network experiences different bandwidth and queuing behavior than a laptop on a wired connection; the specifics of that are their own topic, but the targets should differ, not share one number.
From target to commitment
An SLO is the internal target a team designs to; a service-level agreement (SLA) is the external, often contractual, promise made to a customer. The SLA should sit inside the SLO with room to spare (an error budget: the amount of time the SLO is allowed to be missed before it counts as a real problem), otherwise there is no margin for a bad day.
Worked example
Physics sets a hard floor before any engineering happens. Light in fiber travels at roughly 200,000 km/s (about two-thirds the speed of light in vacuum, due to the refractive index of glass). If a user in Mumbai is served from a single origin server in Virginia, the one-way great-circle distance is roughly 12,000 km:
tone-way=vd=200,000 km/s12,000 km=0.06 s=60 ms
RTTmin=2×tone-way=120 ms
That is the theoretical best case for one round trip before the server does any work at all, and a real page load needs several round trips (DNS lookup, then a TCP/TLS handshake, then the actual request), so a single-origin design cannot hit an aggressive global P95 no matter how fast the backend code is. This is the concrete argument for a content delivery network (CDN, a network of edge servers that cache content closer to users) or a multi-region deployment: it is not a nice-to-have, it is the only way to shrink the distance term in the equation above for users far from wherever the service is deployed.
Trade-offs & pitfalls
- Setting one global latency target and being surprised it's missed for distant regions; the fix is a region-aware target, not "optimize the backend more."
- Optimizing for the average and declaring victory while P95/P99, and the users behind them, stay slow.
- Promising an SLA as tight as the internal SLO, leaving no error budget for a bad day.
- The cost trade-off worth naming explicitly: hitting a tight worldwide P95 costs real money (CDN, edge compute, multi-region infrastructure and replication). "How fast" is really "how much are we willing to spend to move the physical floor closer to zero," and that should be a deliberate decision, not an assumed one.
You're choosing persistence for a user-profile service with frequent reads, moderate writes, flexible attributes, and occasional complex queries involving joins. Would you go SQL or NoSQL here, and why?
Sample Answer
Direct answer
Choose a relational database with a flexible-attribute column, for example PostgreSQL with a JSONB (binary JSON) column, rather than a pure document store. The workload's defining features, frequent reads, moderate writes, and occasional complex queries with joins across related entities, are exactly what a relational engine with atomicity-consistency-isolation-durability (ACID) transactions and a real query planner are built for. A document store would force those occasional joins to be rebuilt in application code, which is a worse trade than tolerating a bit more schema rigidity for the flexible fields.
Structured elaboration
| Criterion | Relational + flexible column | Pure document store |
|---|---|---|
| Consistency | ACID transactions across related tables | Often single-document atomicity only, multi-document transactions vary by product and add complexity |
| Joins / complex queries | Native, indexed, planner-optimized | Rebuilt in application code or via aggregation pipelines |
| Schema flexibility | Flexible column (JSONB) handles optional attributes without migrations | Schema-less by default, easy for evolving fields |
| Scaling | Vertical plus read replicas fit read-heavy, moderate-write loads well | Easier horizontal write scaling, relevant only if writes were much higher |
| Operational overhead | One primary system, mature tooling | Fine alone, but a hybrid adds a second system to run |
Decision criteria to walk through: how often do "occasional" joins actually occur in practice (if frequent, this favors relational strongly); how correctness-sensitive is the data (account state favors strong transactional guarantees); how much of the schema is genuinely unpredictable versus a fixed set of optional fields (JSONB handles the latter well without needing a schema-less engine).
Worked example
A concrete schema: core relational columns, user_id (primary key), email, status, created_at, with foreign-key relationships to organizations and permissions tables to support the join-heavy queries (for example, "list all users in an organization with a given permission"). A JSONB attributes column holds optional or evolving profile fields, indexed with a generalized inverted index (GIN) for filtering on specific attribute keys without requiring a migration every time a new optional field is added.
Decision branch: if the write volume for this profile service later grows to a level one primary node can no longer sustain, that crosses into write-heavy datastore territory (partitioned, write-optimized storage engines) and would call for revisiting this choice; "moderate writes" as stated in this scenario doesn't cross that line, so the relational-plus-JSONB design holds.
Trade-offs & pitfalls
- Choosing a document database by default because "user profile" sounds document-shaped, then discovering the "occasional" joins aren't so occasional in practice, and rebuilding relational logic in application code, is a common wrong turn.
- Over-normalizing the flexible attributes into their own relational tables, when a JSONB column with a targeted index would have been simpler and equally queryable for the actual filter patterns, adds unnecessary schema churn.
- If writes were instead high-concurrency across many independent keys with no cross-record transactions needed, a document or wide-column store would flip this recommendation, this decision is shaped by the workload, not a permanent rule.
- A field that becomes a genuine business invariant (something the system must enforce, not just store) should graduate from the flexible column into a real, constrained relational column, leaving it in JSONB indefinitely trades away the very guarantees the relational choice was made for.
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.