Dinesh/ Blog
systems

System Design Framework — Reqs, Capacity, HLD, Deep-Dive

A worked design-review method grounded in measurable requirements, consistent capacity arithmetic, complete request paths and defensible failure tradeoffs.

21 min readChecking device speech…
Lesson preparation & details

Level: advanced

Designing for scale · Lesson 16 of 20 ↗
Loading this browser’s progress…

By the end, you should be able to

  • Trace requirements to mechanisms and falsifying tests in a time-boxed design review
  • Compute consistent capacity, storage, bandwidth and fanout assumptions
  • Defend feed, photo-upload and cache tradeoffs including failure and operational costs

Bring with you

Sources checked: . A source check does not change the publication or update date.

Editorial review: · What review means

In this article · 17 sections

A design diagram is a proposal, not evidence that the system meets its requirements. The useful skill is making the path from requirement to mechanism to test visible. This chapter uses a four-stage practice structure—requirements, capacity, high-level design and deep dive—with a wrap-up. It is a learning heuristic, not a universal employer rubric or a promise about interview outcomes.

Optional preparation

The original preparation budget was 30 minutes; select a few pointers rather than treating the sum of their advertised runtimes as a promise. The titles below are historical finding labels, not verified video identities, creator endorsements or completion-time guarantees. External availability and unseen contents have not been checked in this review.

Scope the problem before naming products

Start with actors, operations, constraints and non-goals. Ask which user journey matters most and which failures the system must survive. “Fast,” “reliable” and “no lost messages” are not yet testable requirements.

For each requirement state:

  • Actor and operation: who performs which action on which object?
  • Population and measurement boundary: client-observed logical operations, server attempts or internal tasks?
  • Target and window: for example, 99% of eligible reads complete within 200 ms over a rolling day.
  • Correctness/consistency: which observations are allowed after acknowledgement? Is read-your-writes required, and for which clients?
  • Durability/failure domain: which acknowledged writes survive process, disk, zone or region failure?
  • Failure policy: timeout, retry, stale response, queued acceptance, explicit rejection or escalation?
  • Security and operations: tenant isolation, retention/deletion, budget, team expertise and on-call capability.

Google's SLO workbook is a primary reference for measurable good-event contracts. Not every design choice needs a QPS number: legal boundaries, transaction invariants, team ownership and deployment constraints can dominate. Conversely, a numeric target does not prove feasibility without measurement.

If the prompt says “no lost messages,” ask: which acknowledgement counts as accepted, which failures must be survived, how long history is retained, and how offline delivery/retries are reconciled? “Accepted by a front end,” “durably recorded,” “delivered to a device” and “read by a person” are distinct. For billing, distinguish raw usage acceptance from finalized invoice accounting; for a feed, distinguish candidate freshness from ranking latency. Those distinctions change storage and acknowledgement choices.

A 45-minute practice budget

Use five minutes for scope, five for estimates, ten for HLD, twenty for deep dives and five for wrap-up. Adjust to the actual conversation. The budget exists to prevent a favorite subsystem consuming all the time, not to punish sensible iteration.

A 45-minute practice budget
Figure text equivalent — labels and relationships
  • Node “Requirements: 5 min” (square).
  • Node “Estimates: 5 min” (square).
  • Node “HLD: 10 min” (square).
  • Node “Deep dive: 20 min” (square).
  • Node “Wrap-up: 5 min” (square).
  • “Requirements: 5 min” leads to “Estimates: 5 min”. Line: normal; ends: arrow point.
  • “Estimates: 5 min” leads to “HLD: 10 min”. Line: normal; ends: arrow point.
  • “HLD: 10 min” leads to “Deep dive: 20 min”. Line: normal; ends: arrow point.
  • “Deep dive: 20 min” leads to “Wrap-up: 5 min”. Line: normal; ends: arrow point.
  • “Deep dive: 20 min” leads to “Requirements: 5 min”: new constraint. Line: dotted; ends: arrow point.

This replaces a misleading time chart whose numeric “durations” were ambiguous in the original Mermaid syntax. A conversation can revisit assumptions after discovering a failure mode. Say “I am assuming one million daily active users; does that match the problem?” If given a fixed assumption, reason under it; if assumptions contradict each other or imply a safety/correctness impossibility, explain the conflict. “Never challenge a reviewer's number” is not engineering judgment.

At minute twelve, if you are stuck inside one cache algorithm, say: “Let me park that detail, sketch the full request path, then return to the bottleneck.” At minute forty, summarize the design and offer a focused remaining question; pretending there is time for a twenty-minute deep dive only hides the tradeoff.

Estimate to eliminate options, not decorate boxes

Begin with units. users × actions/user/day yields actions/day; dividing by 86,400 yields average, not peak QPS. Peak requires a stated burst factor or measured time distribution. Use average volume for retained annual data unless explicitly estimating a sustained-peak upper bound. Distinguish decimal GB/TB/PB from binary GiB/TiB, logical payload from physical storage, and ingress from egress.

Core relationships:

average QPS = daily events / 86,400
peak QPS = average QPS × stated peak factor
logical storage/day = events/day × bytes/event
retained physical storage ≈ logical retention × replication + indexes/metadata/overhead
bandwidth = events/second × transferred bytes/event
in-flight work ≈ arrival rate × mean time in system (stable matching population)
fanout work/second ≈ writes/second × average delivered recipients/write
cache bytes ≈ cached objects × serialized bytes/object + measured structure overhead

A hit ratio does not directly specify memory size; access skew, object sizes, TTL, admission and eviction determine the working set. A process can be CPU-bound, lock-bound, I/O-bound, memory-bound or constrained by an upstream quota. Google's overload chapter explains why resource cost matters more than request count alone. Measure representative requests and headroom rather than asserting every stateless tier scales linearly.

A worked feed design with internally consistent numbers

Use a hypothetical Twitter-like product, not claims about the real service:

  • 1 billion registered users, 200 million daily active.
  • Each active user creates two posts and opens a home feed twenty times per day.
  • Scope: create post, follow/unfollow, view feed. DMs, ads and search are non-goals for this exercise.
  • Post body budget: assume 300 stored payload bytes per post; a 280-character text limit does not itself imply 300 bytes after Unicode/metadata.
  • Feed can be eventually updated, but acknowledge creation only after the selected durable write boundary; the author should be able to read their own accepted post.

Arithmetic:

QuantityCalculationResult
Posts/day200M × 2400M
Average creates/s400M / 86,4004,629.63
Average feed opens/s4B / 86,40046,296.30
Read/write ratio for these operations20 / 210:1, not 100:1
Illustrative 3× peak creates/s3 × 4,629.6313,888.89
Logical post payload/year400M × 300 × 36543.8 TB

The original 15k/s peak × seconds/year calculation was a sustained-peak scenario (~141.9 TB), not the same average workload. Neither figure includes indexes, replication, follow edges, media, feed entries, tombstones or write amplification. With three copies, the payload alone would be 131.4 TB before those overheads. These numbers motivate examining storage/write/fanout capacity; they do not prove a particular database or shard count is necessary.

Executed arithmetic and sensitivity model

from fractions import Fraction
 
def capacity(dau, writes_per_user, reads_per_user, payload_bytes, peak_factor):
    values = (dau, writes_per_user, reads_per_user, payload_bytes, peak_factor)
    if any(x < 0 for x in values) or peak_factor < 1:
        raise ValueError("nonnegative inputs and peak >= average required")
    writes = dau * writes_per_user
    reads = dau * reads_per_user
    return {
        "write_average": writes / 86400,
        "write_peak": writes / 86400 * peak_factor,
        "read_average": reads / 86400,
        "logical_year_bytes": writes * payload_bytes * 365,
        "read_write_ratio": None if writes == 0 else reads / writes,
    }
 
feed = capacity(Fraction(200_000_000), Fraction(2), Fraction(20), Fraction(300), Fraction(3))
assert feed["write_average"] == Fraction(125000, 27)
assert feed["read_write_ratio"] == 10
assert feed["logical_year_bytes"] == 43_800_000_000_000
photo = capacity(Fraction(40_000_000), Fraction(1, 2), Fraction(20), Fraction(1_000_000), Fraction(4))
assert photo["logical_year_bytes"] == 7_300_000_000_000_000
assert photo["read_write_ratio"] == 40
assert 1_000_000 * 200 * 1000 == 200_000_000_000
larger = capacity(Fraction(2_000_000_000), Fraction(2), Fraction(20), Fraction(300), Fraction(3))
assert larger["write_average"] == 10 * feed["write_average"]
assert larger["logical_year_bytes"] == 10 * feed["logical_year_bytes"]
print("feed=43.8 TB/year; photo=7.3 PB/year; ratios=10 and 40: PASS")

This model proves arithmetic under stated assumptions, not throughput or production cost. Vary the average post size, replication, burst factor and follower distribution separately. Ask which change reverses a decision. A 500 GB dataset might still need partitioning for locality/write isolation; a 5 PB store may still use planned offline scans. Dataset size alone neither rules out distributed consistency nor forbids scans. Multiple bottlenecks can emerge together, and removing one reveals another.

High-level design: trace complete operations

Draw the smallest design that addresses the current requirements. Every arrow names a protocol or queue and an acknowledgement boundary; every store names access patterns, key/index choices and ownership. It is acceptable to start with one database and no queue if the requirements fit. A queue is justified by decoupling, replay or burst tolerance, not a magic QPS threshold.

High-level design: trace complete operations
Figure text equivalent — labels and relationships
  • Node “Clients” (square).
  • Node “Authenticated API tier” (square).
  • Node “Post store” (cylinder).
  • Node “Follow graph” (cylinder).
  • Node “Fanout event log” (square).
  • Node “Idempotent fanout workers” (square).
  • Node “Per-reader candidate cache” (square).
  • “Clients” leads to “Authenticated API tier”: HTTPS. Line: normal; ends: arrow point.
  • “Authenticated API tier” leads to “Post store”: create and read. Line: normal; ends: arrow point.
  • “Authenticated API tier” leads to “Follow graph”: follow relation. Line: normal; ends: arrow point.
  • “Post store” leads to “Fanout event log”: transactional outbox publication. Line: normal; ends: arrow point.
  • “Fanout event log” leads to “Idempotent fanout workers”. Line: normal; ends: arrow point.
  • “Idempotent fanout workers” leads to “Per-reader candidate cache”. Line: normal; ends: arrow point.
  • “Authenticated API tier” leads to “Per-reader candidate cache”: feed read. Line: normal; ends: arrow point.
  • “Authenticated API tier” leads to “Post store”: cache rebuild and authorization recheck. Line: normal; ends: arrow point.

Create path: validate principal/content, choose ID, durably record post plus publication intent in the chosen atomic boundary, then acknowledge. An outbox is one option for avoiding “DB commit succeeds, queue publish fails.” The publisher can retry; consumers deduplicate by stable post/event identity. Merely drawing an arrow between a DB and Kafka does not create an atomic transaction.

Read path: authenticate, fetch cached candidate IDs, hydrate visible posts, filter deleted/blocked/private content under current policy and return a bounded page. A cache entry is not permanent authorization. Handle cache loss with bounded reconstruction; avoid making every reader scan every followee during a cold start. Use pagination/cursors with ordering semantics rather than an unbounded timeline response.

Follow path: update the relationship under a declared consistency policy, then establish how it affects future fanout, historical feed backfill and unfollow/block removal. If the graph and cache lag, the read filter must enforce privacy restrictions before returning data.

Component vocabulary, with the differences that matter

  • Stateless request tier: any healthy instance can handle a request when needed state lives elsewhere. It still depends on pools, caches, database limits and upstream capacity; canary/blue-green releases need compatible state and protocols.
  • Cache: cache-aside, write-through or other patterns need freshness, invalidation, admission and eviction rules. A cache outage should have a bounded fallback path, not a synchronized database flood.
  • OLTP store: transactions and indexed access can express invariants. Isolation levels, replicas, distributed sharding and failover alter guarantees; “Postgres means globally strong consistency” is not sufficient.
  • Wide-column/key-value store: model queries and partition behavior. Cassandra's tunable quorum model is not identical to DynamoDB's consistency options and constraints. Select the actual product/API contract, not a category stereotype.
  • Async transport: Kafka, SQS and Kinesis have different ordering, retention, acknowledgement and replay semantics. A queue buffers finite bursts; it cannot make sustained overload disappear.
  • Object storage/CDN: large bytes can bypass application servers after authorized upload/download setup. Signed URLs are bearer capabilities with lifetime/scope; deletion/revocation and cache behavior need explicit treatment.

Deep dive: fanout, hot keys and failure behavior

Choose one or two important risks based on requirements and estimates, with the reviewer as collaborator. “I propose diving into fanout because a small number of high-follower authors dominates write amplification; would consistency or upload durability be more useful?” is clearer than memorizing a product list.

Fanout on write materializes post IDs into follower candidate lists. Reads are cheaper, but a high-follower post creates many writes, including work for inactive readers. Fanout on read queries followed authors when a reader opens the feed; writes are cheaper but reads merge more sources. A hybrid precomputes for selected authors/readers and merges high-fanout authors at read time. The cutoff depends on activity, fanout cost and freshness targets, not a celebrity's asserted current follower count.

Suppose a synthetic author has 100 million followers and each feed entry costs 16 serialized bytes: one post implies 1.6 GB of raw entry writes before overhead. At an assumed 1 million entries/second worker capacity it takes 100 seconds, ignoring skew and retries. That conditional calculation exposes a freshness risk; it does not assert production worker throughput. The same design must bound event backlog and retry work, partition tasks, deduplicate, handle deletion and prevent an active reader's cache from being evicted continually by bulk fanout.

An “800-item timeline” is an illustrative cap, not an LRU algorithm. A per-reader sorted/list structure bounds entries; a global cache admission/eviction policy controls which readers remain resident. A cold miss needs a reconstruction query/index plan, limits and singleflight/budgeted concurrency. “Rebuild from tweet store” without a way to find followed users' recent posts hides the hard query.

Failure matrix and falsifiable tests

FailureDesign responseTest that can disprove it
API dies after DB commit before responseClient retry uses an idempotency key and obtains the same accepted resultInject crash at acknowledgement seam; assert one logical post
Publisher replays an outbox eventConsumer deduplicates stable IDDeliver duplicate events; assert no duplicated feed entry
Cache unavailableBound fallback/concurrency and degrade clearlySimulate cold cache across many readers; inspect DB budget
Hot author/keyHybrid fanout and partitioned/bounded workSkewed synthetic workload; inspect freshness and queue age
User blocks authorCurrent policy filter prevents leakage despite stale candidatesCache old candidates, change policy, assert denied results
Zone failsSurvive the explicitly promised acknowledged-write boundaryFault-model exercise verifies quorum/replica placement and recovery assumptions

These are design-level tests, not claims this review created a cluster. A real release requires implementations and controlled fault/load testing under ownership and safety gates.

Full photo-sharing drill and worked solution

Use the same phases, but don't copy the feed's assumptions blindly:

# Photo-sharing practice contract (illustrative)
Scope: upload photo, view feed, follow user; exclude DMs/stories/video/ads.
Population: 100M MAU, 40M DAU.
Usage: 0.5 uploads and 20 feed opens per active user per day.
Payload: assume 1 MB per original photo, decimal units.
Targets: upload acknowledgement and feed latency stated at client boundary;
         accepted upload must include durable bytes plus metadata completion.
 
Average uploads: 20M / 86400 = 231.48 per second.
Illustrative 4x peak: 925.93 per second (roughly 1k, not annual average).
Logical original bytes: 20M * 1 MB * 365 = 7.3 PB/year.
Feed opens: 800M / 86400 = 9259.26 per second.
Read/write ratio: 20 / 0.5 = 40:1, not 100:1.
Hot feed IDs: 1M readers * 200 entries * 1KB = 200 GB before overhead.

The original “1k uploads/s all year gives ~30 PB” is approximately a sustained-peak alternative (31.536 PB at decimal 1 MB), not storage for 20 million daily uploads. Add replicas, thumbnails, metadata, logs and retention/deletion policy separately. Egress depends on photos viewed per feed page, delivered image sizes and cache hit distribution—not just feed API calls.

Upload HLD: client asks an authenticated API for a scoped upload capability; bytes go to object storage under size/content controls; completion validates ownership and object status; metadata moves from pending to ready through an idempotent workflow. Scanning/transcoding/moderation are explicit stages with visibility gates. If byte upload succeeds but metadata commit fails, reconcile or expire orphan objects. If metadata says ready before bytes are available, users get broken posts.

Read HLD: API returns authorized metadata/candidate IDs; a CDN serves appropriate image variants with deliberate public/private cache policy. The client does not necessarily route every byte through “CDN → LB → API → S3.” Private signed URLs have expiration and leak risks; a block/delete requirement may limit cache TTL or require stronger delivery authorization.

Deep dive: compare fanout strategies using active followers and write amplification, or investigate the upload state machine and failure seams. Do not say sharded Postgres is always simpler than Cassandra; weigh queries, transactions, partition ownership, team expertise and operational tools.

Wrap-up: identify unresolved moderation/abuse, deletion propagation, hot-author feed latency and egress costs. State which tests and telemetry would validate the next decision. Search can be a later feature only if the agreed scope permits deferral.

Practice template and self-review

# Design review: [problem]
## Requirements
Actors, top operations, population, correctness, consistency, durability,
latency/availability windows, privacy, cost, team constraints and explicit non-goals.
## Estimates
Average QPS with units; separate peak model; logical/physical retention;
bandwidth; fanout and hot-set assumptions; which decision each number changes.
## HLD
Trace create/read/update/delete; label protocols, acknowledgement boundaries,
authority, stores/access keys, caches, async work and error paths.
## Deep dive
Pick one or two consequential risks. Specify schema/API, invariant,
concurrency/failure interleaving, alternatives, recovery and a falsifying test.
## Wrap-up
Three explicit tradeoffs; unresolved risks; measurement/test plan; next feature
or experiment and why it is worth doing before adding more infrastructure.

A reusable sentence is “I choose X for requirement Y, giving up Z; I would revisit if measurement W crosses this bound.” Also ask: why this component rather than a simpler alternative, what fails first under tenfold/skewed demand, and which complexity/consistency/cost did this choice introduce? “Because it is standard” is not an explanation.

Six anti-patterns and how to recover

  1. Skipping clarification: return to actors, scope and failure obligations before adding boxes.
  2. Estimation theatre: show units/arithmetic, ranges and a decision; false precision is not confidence.
  3. Buzzword substitution: explain the actual invariant. If invoking Raft, identify the replicated state and availability/safety boundary; if Kafka, explain partition ordering, consumer retry and retention.
  4. Unproductive assumption fights: accept declared exercise constraints, but flag contradictions and ask a clarifying question when they change correctness.
  5. One-component tunnel vision: trace the whole operation, then choose a justified deep dive.
  6. No wrap-up: state tradeoffs, unsolved risks and the next test rather than pretending the diagram solves everything.

The original Meta/Google/Amazon interview anecdotes and claimed rejection/promotion rubrics lacked primary support. Retain them as practice failure scenarios: overengineering a 100-QPS internal shortener, saying “Raft” without naming what must agree, and spending all 45 minutes on a rate-limit algorithm without a deployment/failure path. No named-company hiring result or historical convergence date is asserted.

Use a local progression rubric instead of universal L4/L5/L6 labels: first complete the request paths with guidance; then drive assumptions, arithmetic and one invariant independently; then compare alternatives and include organizational ownership, on-call burden, migration and reversal costs. Employers' actual levels and review criteria differ. The System Design Primer is an inspectable author's study framework, not an official cross-company rubric or a primary source for production performance.

Stretch: distributed cache in five phases

Requirements: clarify remote cache versus in-process library, key/value sizes, TTL, eviction, persistence, tenant isolation, expected miss behavior and consistency. A cache of reconstructible data has different durability needs from a system whose “cache” is actually the only copy.

Capacity: estimate active key bytes plus overhead, read/write rate, hot-key skew, replication bandwidth and miss-rebuild cost. Large aggregate memory does not solve one overloaded key; ten nodes each with spare memory can still overload the node holding a viral value.

HLD: client-side or proxy routing to partitioned nodes, optional replicas, backing store for reconstructible data and membership/version distribution. Consistent hashing or logical partitions can reduce movement, but clients must agree on routing epochs and handle transition reads. Use authentication and quotas; no public unauthenticated cache.

Deep dive: during resharding, specify old/new owners, copy/catch-up, write routing, read fallback, cutover and cleanup. For writes, distinguish write-through acknowledgement from write-behind buffering; a cache crash before a write-behind flush can lose accepted work unless another durable boundary exists. A consensus store may coordinate membership/configuration; it does not need to replicate every cache access by default.

Wrap-up: hot-key replication/local caching, stampede control, backend protection, measured hit rate/latency, memory/evictions and membership errors are key risks. Name the failure policy if both cache and backend are unavailable. This is enough to defend a design direction without pretending to have built or benchmarked it.

Recall and worked answers

  1. Phases and time? Requirements 5, estimates 5, HLD 10, deep dive 20, wrap-up 5 as a flexible practice allocation.
  2. Why estimate early? Numbers constrain choices and reveal resource risks; nonnumeric correctness/ownership constraints matter too, and iteration is expected.
  3. Three tradeoff phrases? “X buys Y at cost Z”; “A fits this access pattern, B wins if it changes”; “risk R is checked by metric/test T.”
  4. Push/pull/hybrid fanout? Pay work at write versus read; combine when skew/activity makes either pure strategy expensive. Specify visibility and deduplication.
  5. Given one million users? Reason under the declared assumption and clarify active fraction/operations; respectfully resolve contradictions instead of arguing or blindly accepting impossible obligations.
  6. Minute twelve tunnel vision? Park the detail, trace HLD, return to the risk with context.
  7. Three anti-patterns? Any three above, paired with the actual recovery behavior.
  8. What shows growth beyond a guided answer? Independent scope, justified arithmetic, invariant/failure reasoning, tradeoff communication and operational ownership—not a universal employer level label.
  9. Minute forty without depth? Briefly identify the most consequential unresolved risk, ask which bounded angle is useful, and preserve a truthful wrap-up.
  10. Distributed cache? Clarify reconstructibility, estimate memory/skew, draw routing/membership/backing store, deep-dive migration or write guarantees, then name hot-key/stampede/overload tests.

Rehearse with a shortener, chat and feed, then change one assumption in each. The learning test is whether the design changes for a reason you can explain, not whether you reproduce a familiar diagram.

Prerequisites and study route

Bring load balancing, caching, partitioning and failure-domain concepts. Apply the same reasoning to URL shortening, chat delivery, feed fanout and an AI-chat product; the workload and correctness contracts change, so the architecture must change too.

For preparation, revisit load balancers, sharding/partitioning, multi-region failure domains and caching. The original labels “S066 Load Balancers” and “S070 Sharding” do not match the current topic routes; use these topic-matched destinations, not the old numbers. URL shortening is the next complete drill; chat adds routing/delivery, newsfeed emphasizes fanout, and AI chat adds streaming, cost, safety and memory. Those are routes, not a claim that this chapter implements all four products.

Previous original-series stop: S125 · Multimodal LLMs. Next: S127 · Design a URL Shortener. The 6-Month Learning Plan is the original hub.

This retains the original 130-session series context and its suggested 90-minute learning block: intuition → visual walkthrough → hands-on → production/failure reasoning → recall/stretch. The 45-minute design drill is a separate practice timer; repeat it with changed assumptions, optionally record a run and review where time went. Neither duration nor “evergreen” is a completion or currency guarantee.

Sources and verification boundary

Primary source passages were read and captured on 2026-10-08, with raw/canonical hashes, exact quote offsets and claim-specific entailment retained in the batch evidence. Historical pins describe their own versions, not current deployment recommendations.

Exact arithmetic and sensitivity fixtures were executed. Feed/photo/cache designs remain defended design exercises, not measured deployment throughput or employer scoring rules.

Pause / Recall / Apply

Can you explain it without the page?

Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.

Stored in this browser only. No account, no sync. Clearing browser data removes your record.