System Design Problem Solving: A Practical Framework
A repeatable HLD interview framework applied end-to-end to a URL shortener and an order/notification system.
System Design Problem Solving
Everything in the two previous guides — scaling strategy, load balancing, CAP trade-offs, consistency models — is raw material. This guide is about assembling that material into a coherent answer under interview conditions, in 35-45 minutes, without going in circles. The framework below is not the only valid approach, but it is a reliable default: it forces you to establish scope and scale before you draw a single box, and it leaves time for the deep dive that actually differentiates a strong answer from a mediocre one.
1. The Framework
Time allocation for a 40-45 minute interview
| Phase | Time | Goal |
|---|---|---|
| 1. Clarify requirements | 5 min | Scope the problem; avoid designing the wrong system |
| 2. Estimate scale | 5 min | Turn vague scale ("web scale") into numbers that drive real decisions |
| 3. High-level design | 10 min | Draw the major components and how data flows between them |
| 4. Deep dive | 15 min | Go deep on 1-2 components the interviewer (or you) flags as interesting/risky |
| 5. Trade-offs & bottlenecks | 5-10 min | Name what you'd do differently at 10x scale, and known weaknesses |
The single most common failure mode is skipping step 1 and 2 and jumping straight to boxes and arrows. A beautifully drawn architecture for the wrong problem (wrong read/write ratio, wrong latency requirement, wrong consistency need) reads as a candidate who doesn't gather requirements before building — which is exactly the skill the interview is testing. Spend the first five minutes talking, not drawing.
2. Step 1 — Clarify Requirements
Split requirements into functional (what the system does) and non-functional (how well it does it — the qualities that actually drive the architecture). Non-functional requirements are what separate system design from a CRUD app tutorial.
Questions to ask, every time
| Category | Questions |
|---|---|
| Functional scope | What are the core user-facing operations? What's explicitly out of scope? |
| Scale | How many users? Read/write ratio? Peak vs. average traffic? |
| Latency | What's the acceptable p99 latency for the critical path? |
| Consistency | Can this tolerate eventual consistency, or does it need strong guarantees? |
| Availability | What's the target uptime? What happens to the user if a component is down? |
| Durability | Can any data be lost? What's the cost of losing it? |
| Growth | Is this a greenfield system sized for today, or must it anticipate 10x growth? |
Restate the requirements back before moving on, even in a real design doc, not just an interview: "So to confirm: we need to support 100M shortened URLs, ~100:1 read-to-write ratio, redirect latency under 100ms at p99, and we can tolerate a shortened URL taking up to a few seconds to become resolvable after creation." This single sentence anchors everything that follows and gives the interviewer (or your reviewers) a chance to correct a wrong assumption before you've built 20 minutes of design on top of it.
3. Step 2 — Estimate Scale
Convert the requirements into rough numbers for traffic, storage, and bandwidth. The goal isn't precision — it's getting within an order of magnitude so you can make defensible decisions (do we need sharding? do we need a CDN? does a single Postgres instance suffice?).
The estimation checklist
Traffic:
- Requests/second (average and peak)
- Read : write ratio
Storage:
- Bytes per record × number of records = total storage
- Growth rate per year
Bandwidth:
- Requests/sec × average payload size = throughput
Memory (for caching):
- Working set size (hot data) → cache sizing
Keep memorized reference numbers handy — they make estimation fast and defensible: 1 day ≈ 86,400s (round to 100,000s for quick mental math), 1 million requests/day ≈ ~12 req/s average, a UUID is 16 bytes / 36 chars as a string, a typical DB row with a few indexed columns is a few hundred bytes, SSD random read latency is sub-millisecond, a cross-region network round trip is 50-150ms.
4. Step 3 — High-Level Design
Draw the major components and the request flow through them, using the vocabulary from the scalability and CAP guides in this phase: where's the load balancer, is the service tier stateless, where does caching sit, what's the data store and why, is anything async (queue-backed).
Resist the urge to add every component you know exists. A message queue, a CDN, a search index, and a Kafka pipeline all sound impressive, but each one you add without a stated reason is a red flag, not a strength — it signals pattern-matching against "system design buzzwords" rather than reasoning from the requirements you just gathered. Add a component only when you can say, in one sentence, what problem it solves that the simpler design didn't.
5. Step 4 — Deep Dive
Pick the 1-2 components that are actually interesting for this problem — usually wherever the scale, consistency, or latency requirements create real tension — and go deep: schema design, algorithm choice, specific trade-offs, failure handling. This is where the interview is actually won or lost; the high-level design is table stakes.
6. Step 5 — Trade-offs and Bottlenecks
Close by naming what breaks first as scale grows, and what you'd change. This demonstrates you understand your own design's limits rather than presenting it as a finished, flawless artifact.
Worked Example 1: URL Shortener
A classic because it's small enough to fully design in 40 minutes but touches ID generation, caching, read-heavy scaling, and redirect latency — a good showcase of the full framework.
Clarify requirements
- Functional: shorten a long URL → short code; redirecting a short code → 302 to the original URL; optional custom aliases; optional expiration.
- Non-functional: extremely read-heavy (redirects vastly outnumber creations); redirect latency should be very low (sub-100ms, ideally sub-20ms with caching); short codes must be unique; eventual consistency for creation-to-availability is acceptable (a few seconds of propagation delay is fine).
Estimate scale
Assume: 100M new short URLs created per month, 100:1 read:write ratio
Writes/sec (avg) = 100,000,000 / (30 × 86,400) ≈ 39 writes/sec
Reads/sec (avg) = 39 × 100 ≈ 3,900 reads/sec
Peak reads (3x) ≈ 11,700 reads/sec
Storage per URL: short_code(7B) + long_url(~500B) + metadata(~50B) ≈ 560B
5-year storage: 100M/mo × 60 months × 560B ≈ 3.36 TB (small — fits comfortably
on modern SSD-backed DBs,
sharding not yet required)
Short code space: base62 (a-z, A-Z, 0-9), 7 characters
62^7 ≈ 3.5 trillion possible codes — far more than needed
This estimate already tells you two design decisions: (1) storage volume is small enough that a single well-indexed relational database (or a modestly sharded one) is entirely sufficient — you do not need a globally distributed database for this problem, and reaching for one unprompted is a red flag rather than a strength; (2) the read load is what actually needs horizontal scaling and caching, not the write path.
High-level design
Deep dive: ID generation strategy
This is the component worth going deep on — the choice materially affects correctness and scalability.
| Approach | Mechanism | Pros | Cons |
|---|---|---|---|
| Random string + collision check | Generate random base62 string, check DB for existence, retry on collision | Simple | Extra DB round trip per collision; collision rate rises as the keyspace fills |
| Hash of the long URL (MD5/SHA, truncated) | Hash the input URL, take first 7 chars | Deterministic — same URL always gets same code (dedupe for free) | Hash truncation collisions still need handling; doesn't support two different short codes for the same long URL if that's a requirement |
| Auto-increment ID + base62 encode | Central counter (or DB sequence) → encode as base62 | No collisions possible, no retries needed | Central counter is a bottleneck/single point of failure at very high write rates; sequential IDs are guessable |
| Distributed ID generation (Snowflake-style) | Each node generates unique IDs from (timestamp, worker ID, sequence) with no central coordination | No single point of failure, no collision, scales writes horizontally | More moving parts than needed for this problem's actual write volume (39/sec) |
For this problem's actual scale (~39 writes/sec), a central auto-increment counter with base62 encoding is the right, boring answer — it's simple, collision-free, and 39 writes/sec is nowhere near what a single sequence generator (or even a coordinated range-allocator handing out ID blocks to each app server to avoid a DB round trip per write) can handle. Reaching for a Snowflake-style distributed ID generator here is over-engineering relative to the stated scale — but it's the right answer if the interviewer bumps the requirement to 100,000 writes/sec, which is exactly the kind of follow-up that tests whether your design choices are actually scale-aware or just memorized.
Deep dive: caching the redirect path
Since reads dominate 100:1, the redirect path is where latency and infrastructure cost are actually won or lost.
- Cache-aside with Redis: on a cache miss, read from DB and populate the cache; TTL of a few hours to a day, since URLs rarely change once created.
- Cache the hot subset, not everything: real-world short URL access follows a power law — a small fraction of links get the vast majority of clicks. Size the Redis cluster for the hot working set, not the full 3.36 TB dataset; even a few GB of cache captures the overwhelming majority of redirect traffic.
- CDN edge caching for the redirect response itself is viable since a
302for a given short code changes rarely — pushes cache hits even closer to the user and off your own infrastructure entirely for very hot links.
Trade-offs and bottlenecks at 10x scale
- At 10x write volume, a single auto-increment counter becomes the bottleneck — move to range-based ID allocation (each app server reserves a block of 10,000 IDs at a time) to cut DB round trips per write by four orders of magnitude.
- At 10x read volume, add more cache replicas and consider a consistent-hashing cache cluster rather than a single Redis instance, to avoid a single cache node becoming a hotspot.
- Analytics (click counts per link) is intentionally deferred — bolting it onto the synchronous redirect path would add latency to the hot path; the correct answer is to fire an async event (queue) on redirect and aggregate click counts out-of-band.
Worked Example 2: Order and Notification System
A less "toy" example than a URL shortener — this is closer to a real capstone-style backend system and forces you to reason about consistency and asynchronous workflows together, tying back to the messaging and persistence phases earlier in this roadmap.
Clarify requirements
- Functional: a customer places an order; the system reserves inventory, processes payment, persists the order, and notifies the customer (email/SMS/push) of order confirmation and later status changes (shipped, delivered).
- Non-functional: order placement and payment must be strongly consistent (no double-charging, no overselling inventory); notification delivery can be eventually consistent — a notification arriving a few seconds late is fine, and at-least-once delivery (occasional duplicate notification) is acceptable, but losing a notification silently is not.
Estimate scale
Assume: 500,000 orders/day, notifications average 3 per order (confirmation,
shipped, delivered)
Orders/sec (avg) = 500,000 / 86,400 ≈ 5.8 orders/sec
Peak (5x, e.g. sale event) ≈ 29 orders/sec
Notifications/sec (avg) = 5.8 × 3 ≈ 17.4/sec
Notifications/sec (peak) ≈ 87/sec
These numbers are modest — this is not a "millions of requests per second" problem. The interesting design challenge here isn't raw throughput, it's correctness under partial failure: what happens if payment succeeds but the notification service is down? What happens if the process crashes after reserving inventory but before charging payment? That's the deep dive worth spending time on, not load-balancer topology.
High-level design
Deep dive: consistency boundary between order placement and notification
This is the crux of the design. Order placement (inventory + payment) and notification delivery have fundamentally different consistency requirements, and the architecture should reflect that split explicitly rather than treating the whole flow as one transaction.
| Step | Consistency need | Mechanism |
|---|---|---|
| Reserve inventory | Strong — must not oversell | Single DB transaction with a row-level lock or optimistic concurrency check on stock count |
| Charge payment | Strong, and must be idempotent | Synchronous call to payment gateway with an idempotency key derived from the order ID, so a client retry (or our own retry after a timeout) never double-charges |
| Persist order as CONFIRMED | Strong — this is the durability record of the transaction | Same DB transaction as inventory reservation, committed only after payment gateway confirms |
| Notify customer | Eventual, at-least-once | Publish an event to a queue after the order is durably committed; a separate notification service consumes it asynchronously |
The order that operations happen in matters as much as their individual consistency guarantees. If you publish the "OrderConfirmed" event before the DB transaction commits, a crash between the publish and the commit produces a notification for an order that doesn't durably exist. The correct sequencing is: commit the order state first (this is your source of truth), then publish the event — using the transactional outbox pattern (write the event to an outbox table in the same DB transaction as the order, then have a separate relay process publish it to the queue) to guarantee the event is eventually published if and only if the order was actually committed.
Deep dive: handling partial failure
Payment gateway timeouts are the trap most designs miss. A timeout doesn't tell you whether the charge succeeded or failed — the request may have succeeded on the gateway's side while the response was lost in transit. Naively retrying on timeout risks a double charge; naively treating timeout as failure risks charging the customer while your system thinks it failed and never fulfills the order. The idempotency key is what makes it safe to re-query ("did this order_id already get charged?") or safely retry — this single detail is a strong signal of design maturity in an interview.
Deep dive: notification delivery guarantees
- At-least-once delivery, deduplicated on the client side where possible: the notification service should assume a message may be redelivered (consumer crash after processing but before acknowledging the queue message) and design notifications to be safe to send twice — or track a delivery-attempted flag keyed by (order_id, notification_type) to skip true duplicates.
- Retry with exponential backoff for transient provider failures (email/SMS gateway briefly unavailable), and a dead-letter queue after N failed attempts so a permanently-failing notification doesn't block the queue or get silently dropped — it gets surfaced for investigation instead.
- Fan-out per channel: treat email, SMS, and push as independent, parallel deliveries rather than a single sequential pipeline, so a slow or down SMS gateway doesn't delay the email that would otherwise have gone out immediately.
Trade-offs and bottlenecks at 10x scale
- At 10x order volume, the single strongly-consistent order/inventory database becomes the constrained resource — the mitigation is sharding by a key like
customer_idorwarehouse_id(whichever aligns with how inventory is actually partitioned in the business), not weakening the consistency guarantee, since overselling remains unacceptable at any scale. - The notification service, being stateless and queue-driven, scales horizontally by simply adding more consumers — this is intentionally the easy part of the design, which is exactly why the strongly consistent order core deserved the deeper design attention.
- At extreme scale, consider read models: a denormalized, eventually-consistent "order history" read view (updated via the same outbox events) so customer-facing order-status queries don't compete for capacity with the transactional write path.
7. Common Mistakes That Sink an Otherwise Good Design
| Mistake | Why it hurts | Fix |
|---|---|---|
| Jumping to architecture before clarifying requirements | Designs the wrong system; wastes the most valuable early minutes | Spend the first 5 minutes only talking |
| No numbers anywhere in the design | Every claim ("this scales well") is unfalsifiable | Tie every major decision to an estimate |
| Treating the whole system as one consistency model | Over-engineers simple data, under-engineers critical data | Classify each data type's consistency need explicitly |
| Adding components with no stated justification | Reads as buzzword pattern-matching, not reasoning | Justify every box in one sentence |
| Never mentioning failure modes | A design with no failure handling isn't a real design | Proactively call out at least 2-3 failure scenarios and how they're handled |
| Treating the design as finished/perfect | Signals lack of production experience | Close with explicit trade-offs and what breaks at 10x |
| Silence during thinking time | Interviewer can't follow or correct your reasoning | Narrate your thought process, even when unsure |
Key takeaways
- The framework — clarify, estimate, design, deep-dive, trade-offs — exists to force requirements and scale to drive the architecture, not the other way around.
- Non-functional requirements (latency, consistency, availability, durability) are what actually differentiate one valid design from another; functional requirements alone rarely do.
- Back-of-envelope estimates should change at least one concrete decision (do we need sharding, caching, a queue) — an estimate that doesn't is a wasted five minutes.
- The deep dive is where interviews are won — pick the 1-2 components with genuine design tension for this specific problem, not the ones you rehearsed.
- Different data within the same system can and should have different consistency guarantees — a strongly consistent order core and an eventually consistent notification pipeline are not a contradiction, they're good design.
- The transactional outbox pattern is the standard, defensible answer to "how do you atomically commit state and publish an event about it."
- Idempotency keys are the answer to "what if the network fails between the request and the response" for any operation that must not be duplicated.
- Always close by naming what breaks first at 10x scale — it demonstrates you understand your design's limits rather than presenting a false sense of completeness.
Interview Questions
- Walk through the framework you use to approach a system design interview question, end to end.
- Why is it important to clarify non-functional requirements before starting the high-level design?
- How would you estimate the number of requests per second a system needs to handle, given daily active users and an average actions-per-user number?
- Design a URL shortener. What ID generation strategy would you use, and how would your answer change at 1,000x the write volume?
- Why does a URL shortener's design lean so heavily on caching the redirect path specifically, rather than the creation path?
- Design an order placement system that also sends order-status notifications. Which parts need strong consistency and which can be eventually consistent, and why?
- What is the transactional outbox pattern, and what specific bug does it prevent?
- How would you handle a payment gateway request that times out — where you don't know if the charge succeeded?
- What does "at-least-once delivery" mean for a notification system, and how do you make duplicate notifications safe?
- What would you change about your order system's design if order volume grew 10x?
- How do you decide which 1-2 components deserve a deep dive in a 40-minute design interview?
- What's a concrete example of over-engineering a design, and how would you recognize you're doing it mid-interview?
- Why is "we'll use a message queue" not a complete answer on its own — what follow-up questions should you expect and be ready to answer?
- How would you explain, using the CAP/PACELC vocabulary, why an order's payment step is synchronous but its notification step is asynchronous?