Skip to content
Software Atlas

The System Design Method

How to go from a blank page to a design you can defend: pin down the requirements, estimate the load with round numbers, sketch the API and data, start simple, then let the numbers tell you where it breaks.

⏱ ~45 min read14 sections7 interactive widgetsGo · JavaScript
FoundationsPractitionerStaff-level
Foundations§01

The 30-second version

In one sentenceSystem design is turning a vague request ("build us a snippet-sharing site") into a design you can defend: every box on the whiteboard is there because a requirement or a number asked for it.

Whether it's a design review at work or a system design interview, it starts the same way: someone describes a product in two sentences, and you have an empty whiteboard. There are many good designs and no single right one. What people judge is how you got there: did you ask what matters, check the size of the problem, start simple, and notice what breaks?

The method, as a loop

This whole topic teaches one loop. You'll use it on every case study in this area:

The system design loop
  1. 1
    Requirements§02
    What must it do? How fast, how available, how durable? What's out of scope?
  2. 2
    Estimate§03–04
    How many requests a second at peak? How much data per year? Read-heavy or write-heavy?
  3. 3
    API + data model§05
    Which calls does it answer, with what load each? What is stored, and how is it looked up?
  4. 4
    Simplest design, then the bottleneck§06–08
    Draw the request path. Which box runs out first? Change one thing, check the numbers again.
  5. 5
    Failure§09–10
    For every box: what do users see when it dies, and how does the system recover? Which SLOs and capacity keep it safe?
  6. 6
    Write it down§11
    Requirements, numbers, design, alternatives, risks: a design doc others can challenge.
↻ Repeat. Steps 4 and 5 loop until the design meets the requirements. Any step can send you back to an earlier one: a surprising number in step 2 is a reason to re-check step 1.

Two things about the loop matter more than any single step:

  • It's a loop, not a checklist. A number from step 2 can send you back to step 1 ("wait, do we really need to keep snippets forever?"). The bottleneck you find in step 4 might change the data model from step 3.
  • Start with the simplest design that could work, often one app server and one database, and only add a box when a number or a requirement demands it. A Cache you can't justify is just one more thing that can break.

The running example

To make every step concrete, we'll design one small product all the way through: SnipShare, a site where you paste a piece of text or code and get a short link anyone can open. It's simple enough to hold in your head, but it still needs most of the building blocks: storage, caching, search and a CDN.

Foundations§02

Step 1: Pin down the requirements

In one sentenceBefore drawing a single box, find out what the system must do and how well it must do it. Five minutes of questions saves hours of designing the wrong thing.

"Build a snippet-sharing site" sounds clear, until you ask: can people edit a snippet? Do snippets expire? Are there private ones? Is it a hobby project with 1,000 users or a public site with 10 million? Each answer leads to a different design, so asking is part of the work, not a delay before it.

Two kinds of requirement

F

Functional: what it does

Functional requirements are features, said from the user's side. "A user can paste text and get a link." "Anyone with the link can read the snippet." If one is missing, the product is missing a feature.

N

Non-functional: how well

Non-functional requirements are qualities: how fast, how often up, whether data may be lost, how fresh reads must be, and what it may cost. They decide the architecture.

✗

Out of scope: what it won't do

A short list of things you've agreed not to build in this design: accounts, comments, analytics. Writing it down stops scope from quietly growing while you design.

Beginners tend to list features and stop. But two products with identical features can need completely different designs because of their non-functional requirements. These are the five that come up every time:

QualityThe question to askSnipShare's answer
LatencyHow fast must a request feel?Opening a snippet: under 200 ms for most users
AvailabilityWhat share of the time must it work?99.9% (about 43 minutes of downtime a month)
DurabilityCan we ever lose data that was saved?No: a saved snippet must never vanish
ConsistencyAfter a write, how soon must every reader see it?The author sees it at once; others within a few seconds is fine
CostWhat's the budget, in money and in people?Small team; prefer managed services over running our own

"Consistency" here means one specific thing: how stale a read is allowed to be. With copies of the data on several machines, a reader might hit a copy that hasn't received the latest write yet. CAP & PACELC covers this properly; for now, "fresh within a few seconds is fine" is a perfectly good requirement.

Ask the questions that change the design

Not every question is worth asking. A good clarifying question is one whose answer would change a box on the diagram. Click each question to see what it decides:

Questions that change the design
Click a question to hear the product owner's answer and what it decides.
Not worth asking yet: “which language?”, “which cloud?”, “Kafka or RabbitMQ?”. Their answers don't change the shape of the design.

SnipShare's requirements, written down

After the questions, write a short list everyone can see. This is the version we'll design against for the rest of the topic:

✕ Vague
requirements (first try)
- users can share snippets
- it should be fast
- it should scale
- it should be reliable
✓ Designable
requirements.md
Functional
- paste text (up to 1 MB) → get a short link
- anyone with the link can read it
- optional expiry: 1 day / 1 week / never
- search public snippets by keyword

Non-functional
- 10M daily active users, ~10 reads per user per day
- read p99 < 200 ms, write p99 < 1 s
- 99.9% available; saved snippets never lost
- others may see a new snippet up to a few s late

Out of scope (this version)
- accounts and login, comments, edit history

The left version can't be wrong, which is exactly the problem: it gives you nothing to check a design against. The right one gives you numbers to estimate with (next section), a size limit that affects storage, and a scope line that keeps login out of the discussion.

Foundations§03

Step 2: Back-of-the-envelope numbers

In one sentenceA Back-of-the-envelope estimate estimate answers one question in a minute: does this need one server or a thousand? Being within 10× (the same order of magnitude) is the goal; precision comes later.

The numbers decide the design. 10 requests a second fits on a laptop; 100,000 requests a second needs caches, many servers and probably split-up data. You can't tell which one you're building until you multiply a few round numbers together.

The three facts that make it easy

1. Work in powers of ten. Round every number to 1, 2, 3 or 5 times a power of ten, then multiply the leading digits and add up the zeros. 10 million users × 10 reads each = 107 × 101 = 108 reads a day. Nobody cares that it's really 97 million.

2. A day has 86,400 seconds, and you round that to 105. So to turn "per day" into "per second", divide by 100,000 (move the decimal point five places left). It's about 15% off, which is fine for this purpose.

Per day÷ 105≈ per second
1 million (106)106 / 10510
100 million (108)108 / 1051,000
1 billion (109)109 / 10510,000

3. Byte sizes go up by 1,000 each step. For estimates, use powers of ten: 1 KB = 103 bytes, 1 MB = 106, 1 GB = 109, 1 TB = 1012, 1 PB = 1015. (Strictly, a KiB is 1,024 bytes; at this precision the difference doesn't matter.)

Average vs peak

Traffic is never spread evenly over the day. People use SnipShare during working hours, a link gets shared on a forum, Monday morning is busier than Sunday night. The average QPS (queries, or requests, per second) tells you how much work there is in total; the peak tells you how many servers you need, because servers have to survive the busiest minute, not the average one.

The usual shortcut is a peak factor: peak QPS = average QPS × 2 to 10. Consumer products often use 2–3 for normal daily swings; something with bursty events (a ticket sale, a sports final) can be 10× or more.

Worked example: SnipShare

From the requirements: 10 million daily active users (DAU), each reads about 10 snippets a day, and about 1 in 10 users creates one snippet a day. The average snippet is about 10 KB. All numbers are approximate.

1

Requests per day

Reads: 107 users × 10 = 108/day
Writes: 107 × 0.1 = 106/day
Read:write ratio: 108 / 106 = 100 : 1

2

Per second

Reads: 108 / 105 = ≈ 1,000 QPS avg
Peak (× 3): ≈ 3,000 QPS
Writes: 106 / 105 = ≈ 10 QPS, peak ≈ 30

3

Storage and bandwidth

Per day: 106 × 10 KB = 1010 B = 10 GB
Per year: 10 GB × 365 ≈ 3.65 TB (call it 4)
Out at peak: 3,000 × 10 KB = 30 MB/s

Now read what the numbers say, because that's the point of doing them:

  • 3,000 reads a second is a modest load. A single well-indexed database can often serve a few thousand simple lookups a second (measure your own hardware before you rely on it). Add a cache and there's lots of room. No need for anything exotic on day one.
  • 30 writes a second is tiny. Splitting the database across machines to handle writes would be solving a problem SnipShare doesn't have.
  • The system is Read-heavy workload (100 reads per write), so effort should go into making reads cheap: caching and copies of the data, not faster writes.
  • 4 TB a year is the number to watch. Over five years that's 20 TB. And real systems keep 2–3 copies of every byte (replicas and backups, so one dead disk loses nothing): 20 TB × 3 ≈ 60 TB. That's far more than you'd want on database servers, and a hint to keep snippet bodies somewhere cheap and big, like Object storage (§05), which keeps its own copies for you.
  • 30 MB/s is about 240 Mbit/s (×8 to turn bytes into bits). That's fine for a server's network card, and much of it can be served by a CDN anyway.

Your turn

Change the inputs and watch every formula update. Try a 10× bigger product, or make snippets 1 MB instead of 10 KB, and see which number changes the design.

Capacity estimator
Move the sliders. Every result shows its formula. All results are approximate.
Inputs
Results86,400 s/day ≈ 10⁵
Reads per day10 M users × 10100 M
Writes per day10 M users × 0.11 M
Avg read QPS100 M ÷ 86,4001,157(shortcut ÷10⁵ ≈ 1,000)
Peak read QPS1,157 × 33,472
Peak write QPS1 M ÷ 86,400 × 334.7
Read:write ratio10 ÷ 0.1100 : 1
New data per day1 M × 10 KB10 GB
New data per year10 GB × 3653.65 TB
Total stored3.65 TB × 5 yr × 3 copies54.8 TB
Bandwidth out (peak)3,472 reads/s × 10 KB34.7 MB/s
Bandwidth in (peak)34.7 writes/s × 10 KB347 KB/s
• Load: a few app servers behind a load balancer; a cache keeps the database comfortable.
• Storage: too much for database disks. Keep the big bodies in object storage (it stores copies for you); the database keeps only small metadata rows.
• Read:write ≈ 100 : 1, read-heavy. Spend effort on caches and copies of the data.
• Bandwidth: 34.7 MB/s out at peak (×8 ≈ 278 Mbit/s).
The thresholds are rough rules of thumb for this widget, not real limits. Real capacity depends on your hardware and queries: measure it.

The same estimate in code

Doing it by hand is the skill to build, but a tiny script is handy when the inputs keep changing. This runs as shown and prints the exact (unrounded) numbers. (New to Go? The five-minute primer in Async / Await §02 covers everything used here.)

estimate.go
package main

import "fmt"

func main() {
	const (
		dau           = 10_000_000 // daily active users
		readsPerUser  = 10         // snippets viewed per user per day
		writesPerUser = 0.1        // 1 in 10 users creates one snippet a day
		peakFactor    = 3          // busiest hour vs the daily average
		bytesPerSnip  = 10_000     // 10 KB average snippet
		secondsPerDay = 86_400
	)

	readsPerDay := dau * readsPerUser
	writesPerDay := dau * writesPerUser

	readQPS := float64(readsPerDay) / secondsPerDay // ← per day ÷ seconds per day
	writeQPS := writesPerDay / secondsPerDay
	storagePerYear := writesPerDay * bytesPerSnip * 365 // bytes

	fmt.Printf("reads:   avg %.0f QPS, peak %.0f QPS\n", readQPS, readQPS*peakFactor)
	fmt.Printf("writes:  avg %.0f QPS, peak %.0f QPS\n", writeQPS, writeQPS*peakFactor)
	fmt.Printf("storage: %.2f TB per year\n", storagePerYear/1e12)
	fmt.Printf("egress:  %.0f MB/s at peak\n", readQPS*peakFactor*bytesPerSnip/1e6)
}
output (both programs)
reads:   avg 1157 QPS, peak 3472 QPS
writes:  avg 12 QPS, peak 35 QPS
storage: 3.65 TB per year
egress:  35 MB/s at peak

Compare the exact output with the mental math: 1,157 vs "≈ 1,000", 35 MB/s vs "≈ 30". Rounding 86,400 to 100,000 makes estimates about 15% low, which is well within "the right order of magnitude". If an estimate and a script disagree by 10×, one of them has a mistake; if they disagree by 15%, both are fine.

Foundations§04

Latency numbers to keep in your head

In one sentenceYou don't need exact timings. You need four rungs: RAM ≈ 100 ns, SSD ≈ 100 µs, the next machine ≈ 0.5 ms, another continent ≈ 100 ms. Each one is hundreds to thousands of times slower than RAM, and the jump to another continent dwarfs everything else.

The previous section estimated how much work there is. This one is about how long each piece of work takes, which is what users feel as Latency. A well-known list called "latency numbers every programmer should know" (popularised by Jeff Dean and Peter Norvig in the 2000s) gives the rough cost of common operations. The exact values change with every hardware generation; the ratios have stayed roughly the same, and the ratios are what you design with.

The ladder

Each row is one operation. Toggle between the two scales: the log scale (each grid line is 10× more) lets you see every row; the linear scale shows how they really compare, where everything faster than a network round trip shrinks to an invisible sliver.

The latency ladder, and a 200 ms budget
Bars show how long one operation takes. Use “+ add to request” to build one request, step by step. All numbers are approximate.
L1 CPU cache read≈ 1 ns
RAM read≈ 100 ns
Random SSD read (4 KB)≈ 20–100 µs
Cache read (e.g. Redis) in the same data center≈ 0.5 ms
Simple indexed DB query, same data center≈ 1 ms
Spinning-disk seek≈ 10 ms
Round trip to another continent≈ 80–150 ms
Your request, calls one after anotherbudget 200 ms
0 ms of 200 ms
Empty. Add a cache read and a DB query to start.
Real requests also spend time on the user's own network and on rendering; this only counts the server's calls.
Typical round numbers for modern servers. Real values vary 2–5× with hardware, cloud, distance and load.

The four rungs worth memorising, in round numbers:

OperationRoughlyCompared with RAM
Read from RAM (main memory)100 ns1×
Random read from an SSD20–100 µsabout 1,000× slower
Round trip to another machine in the same data center0.5 ms (500 µs)about 5,000× slower
Round trip to another continent80–150 msabout a million times slower

(1 ms = 1,000 µs = 1,000,000 ns. The CPU caches topic goes deeper on the fast end of the ladder.)

What the ladder means for a design

A network hop costs more than almost any computation. An app server can do thousands of in-memory operations in the time one call to the database takes. So the first question about a slow request is usually "how many network calls does it make, one after another?", not "is the code efficient?".

You can't beat the speed of light. Light in fiber needs roughly 70–80 ms to cross the Atlantic and come back, no matter how fast your servers are. If SnipShare's servers are in the US and its readers are in Europe, every request pays that before any work happens. The only fix is to put copies of the data closer to the user: a CDN, or servers in more than one region.

Calls in a row add up; calls side by side don't. A request that makes 5 database calls one after another waits 5 round trips. If the calls don't depend on each other, making them at the same time waits about 1. (That's the async patterns topic: Async / Await.)

Spending a latency budget

SnipShare's requirement says "open a snippet in under 200 ms". That 200 ms is a latency budget: every step of the request spends part of it. In the widget above, use the + add to request buttons to build a request one call at a time, and watch the budget bar fill.

Try these and see which ones fit:

  • One cache read, then one database query, both in the same data center. (Easily: about 1.5 ms of the 200.)
  • Fifty database queries one after another, the classic N+1 queries mistake. (Still fits, barely noticeable at 50 ms, but it grows with the page.)
  • Two round trips to another continent. (160–300 ms depending on the distance; the widget uses the 150 ms upper end, so 300 ms: over budget, or nearly, before the server does any work.)
Practitioner§05

Step 3: API and data model first

In one sentenceBefore drawing architecture, write down the requests the system answers and the data it keeps. The boxes exist to serve those two lists.

It's tempting to start with the exciting part, the diagram. But a diagram without an API is a guess: you don't know which requests are hot, which ones must be fast, or what each box actually stores. Writing the API and the data model first takes five minutes and turns the rest of the design into answering concrete questions.

The API, with numbers on it

Three endpoints cover SnipShare's functional requirements. The trick is the last column: copy the load from your estimate onto each endpoint. It shows at a glance where the design effort must go.

EndpointDoesPeak load (§03)Must be
POST /snippetsSave a snippet, return its short id≈ 30 QPSDurable (never lose it), under 1 s
GET /snippets/{id}Return one snippet≈ 3,000 QPSFast: p99 under 200 ms
GET /search?q=…Find public snippets by keyword≈ 50 QPS (a guess: 1 in 60 views)Fresh within minutes is fine

One endpoint carries about 97% of the traffic (3,000 of about 3,080 requests a second). That single observation tells you most of the design: make GET /snippets/{id} cheap, and the other two can stay simple.

the two main calls
POST /snippets
  body:  { "content": "fmt.Println(…)", "expiresIn": "1w", "public": true }
  reply: 201 Created  { "id": "aZ3kQ9", "url": "https://snip.example/aZ3kQ9" }

GET /snippets/aZ3kQ9
  reply: 200 OK  { "id": "aZ3kQ9", "content": "…", "createdAt": "2026-10-31T09:12:00Z" }
         404 Not Found  if it never existed or has expired

How to name resources, pick status codes and paginate search results is its own topic: API Design. For a system design, the list of calls plus their load is enough.

The data model

Next, what do you store? Write it as tables (or documents), with the fields that matter and how big each record is:

schema.sql (Postgres)
CREATE TABLE snippets (
  id          text PRIMARY KEY,      -- the short id in the link, e.g. 'aZ3kQ9'
  created_at  timestamptz NOT NULL,
  expires_at  timestamptz,           -- NULL = never expires
  is_public   boolean NOT NULL,
  size_bytes  integer NOT NULL,
  body_key    text NOT NULL          -- where the content lives in object storage
);
-- ~100 bytes per row: 1M new rows a day ≈ 100 MB/day ≈ 36 GB/year

Notice what's not in the table: the snippet's content. The estimate said bodies add up to about 4 TB a year, while the metadata rows add up to only about 36 GB. Keeping the big, rarely-changing bodies in Object storage (cheap, almost unlimited storage for files, addressed by a key) keeps the database small and fast. The body_key column points to the object.

Access patterns choose the database

An Access pattern is a way the application reads or writes data: "fetch one snippet by id", "search public snippets by word", "delete expired ones". List them, and the storage choice usually becomes obvious:

Access patternHow oftenNeeds
Get snippet by id≈ 3,000/s at peakA lookup by primary key: any database does this well
Create a snippet≈ 30/sA durable insert, and an id that's never handed out twice
Search public snippets by keyword≈ 50/sFull-text search: a job for a Search engine, not LIKE '%word%'
Delete expired snippetsa background job, hourlyAn index on expires_at

Lookups by key at modest volume mean almost any database works, so pick the one the team already runs well (here, Postgres). "Which database?" is rarely the interesting question in a design; the access patterns are. SQL vs NoSQL walks through matching patterns to databases.

Practitioner§06

The building blocks

In one sentenceAlmost every design is built from the same eight blocks. The skill isn't knowing them; it's knowing which problem each one fixes, so you only add it when that problem shows up.

Each block below has its own deep-dive. This page is the index: what it is in one line, the symptom that tells you to reach for it, and what it costs you. Every block fixes one problem and adds at least one new one, usually another thing that can fail or go stale.

⚖️
Load balancerA Load balancer spreads requests across several identical app servers and skips dead ones. Reach for it when one app server isn't enough, or when one server dying must not take the site down. Costs: servers must keep no per-user state in memory. Load balancing →
⚡
CacheA Cache keeps copies of hot data in memory (often in Redis) so most reads skip the database. Reach for it when reads are repeated and the database is busy or slow. Costs: readers can see stale data, and a cold or dead cache can flood the database. Cache-aside →
🌍
CDNA CDN is a network of servers around the world that keeps copies of responses near the users. Reach for it when users are far away, or the same public content is read a lot. Costs: you must control what's cacheable and how long. Caching layers →
📚
Read replicasA Replica is a full copy of the database on another machine, kept up to date from the main one. Reach for it when reads overload one database, or you need a standby to take over. Costs: replicas lag behind, so a read right after a write can miss it. Replicas do not add write capacity. Replication →
🧩
ShardsA Shard is one slice of the data on its own database server; together the shards hold everything. Reach for it when writes or total data outgrow one machine. Costs: queries across shards are hard, and a popular key can overload one shard. Usually the last block you add. Sharding →
📨
QueueA queue (run by a Message broker) holds work to be done later by background workers. Reach for it when a request triggers slow work the user needn't wait for, or traffic arrives in bursts. Costs: the work becomes asynchronous: it can fail later, run twice, or run out of order. Queues →
🪣
Object storageAn Object storage service (S3 and similar) stores files by key, cheaply and almost without limit. Reach for it when you store big blobs: images, uploads, large text. Costs: slower per request than a database, and no queries beyond "get by key". Managed services →
🔎
Search indexA Search engine (Elasticsearch, OpenSearch) builds an index from each word to the documents containing it. Reach for it when users search text by keyword. Costs: it's a second copy of the data that must be kept in sync, usually a little behind. (No dedicated topic yet; CQRS projections → covers keeping such a copy up to date.)

Symptom → block

When the numbers or a failure point at a problem, this is the usual first move. It's a starting point, not a rule:

You notice…First reach forSection
One app server is at 100% CPUMore app servers behind a load balancer§07
The database is busy with the same reads over and overA cache§07
Reads are varied (little repetition) and still too manyRead replicas§07
Writes, or total data, too big for one databaseShards§07
Users far away see slow pages; lots of public contentA CDN§04
A request does slow work the user doesn't need to wait forA queue + workers§09
Large files filling the databaseObject storage§05
LIKE '%word%' queries scanning everythingA search index§05

SnipShare with every block

Here's where SnipShare could end up: the read path runs top to bottom (browser → CDN → load balancer → app → cache, then the database and object storage on a miss), and saving a snippet also drops a job on the queue so a worker can add it to the search index. It's not where it starts: §07 grows it from one server, one box at a time, and you'll see that at SnipShare's real numbers, several of these boxes aren't needed yet.

SnipShare with every block (where it could end up, not where it starts)
👥 browser
CDN
Load balancer
App servers ×2
Cache (Redis)
Postgres primary
Read replica
Object storage
Queue
Indexer worker
Search index
Practitioner§07

Step 4: Start simple, find the bottleneck, change one thing

In one sentenceDraw the path one request takes. Find the one box that runs out first. Fix only that box, then look again, because fixing one bottleneck always reveals the next.

Start with the simplest thing that works

The first design should be embarrassingly simple: one app server and one database. For SnipShare's real numbers (about 3,000 reads a second at peak) it isn't quite enough, as you'll see below, but it's the right starting point, because every box you add later will then have a reason written next to it.

Starting big has real costs. Each extra box is something to deploy, monitor, pay for and debug at 3 a.m., and the design becomes harder to change. "We might need it later" is how teams end up running a cluster of message brokers for 20 requests a second. (The principle has a name, YAGNI: you aren't gonna need it.)

Draw the request path

The request path is every box one request passes through. For the simple design, reading a snippet goes: browser → app server → database → app server → browser. Now put the load from §03 on each box and compare it with what the box can handle. The box whose load is closest to its limit is the Bottleneck, and it sets the maximum for the whole system, no matter how much room the others have.

A useful way to say "how close to its limit" is Utilization: load ÷ capacity. A server that can handle 5,000 requests a second and gets 4,000 is 80% busy. As a box gets close to fully busy, requests start queueing and latency shoots up long before it hits 100%; Worker Pools shows why. So this topic uses a simple target:

The loop, by hand

  1. Draw the request path with the load on each box.
  2. Find the box with the highest utilization.
  3. Change one thing that relieves that box: a component from §06.
  4. Recompute the numbers. Repeat until every box is under the target at peak and nothing is a single point of failure.

Changing one thing at a time matters for the same reason it matters when debugging: if you add a cache, three replicas and a CDN at once, you can't tell which one helped, and you're paying for all three.

Try it: grow SnipShare

The simulator starts as the simplest design at 100 QPS. Drag the traffic up. Each time a box turns red, add one component to fix it, and watch where the red moves next. Then switch to 10 : 1 reads (a more write-heavy product) and try again.

Find the bottleneck
Drag the traffic up. When a box turns red, add the component that fixes it, then check again. Capacities are made-up round numbers.
= 99 reads/s + 0.99 writes/s
👥 users
App server×1
2% busy
100/s each
Database1 × (1)
2% busy
99 reads + 0.99 writes /s per node
Every box is under the 60% target. Users are happy.machines you run: 2
Note: with one app server and no load balancer, that one server is a single point of failure (§09).
Target: every box under 60% at peak (amber at 60%, red over 100%). Simulator assumptions (made up, not measured): an app server handles 5,000 QPS; a database node 5,000 reads/s or 2,000 writes/s; the cache hits 90%; the CDN answers 60% of reads. Real numbers depend entirely on your code and hardware.

Things to notice while you play:

  • The fix depends on the cause. A database overloaded by repeated reads wants a Cache. A database overloaded by writes doesn't care about the cache at all.
  • Replicas don't add write capacity. Every Replica applies every write the primary does, so with heavy writes the "per node" write load never drops. Only splitting the data into shards divides writes. Sharding explains how, and why it's usually the last thing you reach for.
  • You can't add a second app server without a load balancer. Users need one address to connect to; the load balancer is that address, and it spreads requests across the servers behind it.
  • The cost line goes up with every box. Watch "machines you run". At 3,000 QPS, a sensible design is a handful of machines. If you're running more, the simulator tells you which ones the numbers don't need.

SnipShare at its real numbers

Back to the estimate: about 3,000 read QPS and 30 write QPS at peak, read-heavy. Running the loop by hand (with the simulator's capacities, which are made up):

RoundBottleneckChangeWhy this and not something else
1Database: ≈ 2,970 reads/s + 30 writes/s ≈ 61% busy (the app server is right behind it at 60%)A cacheSnippets never change and the same popular ones are read over and over: ideal for caching. The database drops to ≈ 7%
2App server: 3,000 QPS on one box = 60%, and it's a single point of failureLoad balancer + a second app serverEach server drops to 30%, and one can now die without taking the site down
3Nothing over ≈ 30%StopThe numbers don't ask for replicas, shards or a CDN, yet

That's the honest answer for today's SnipShare: two app servers, a load balancer, a cache and one database (plus object storage for the bodies, from §05). A read replica will come back in §09, but for failure, not for load.

Practitioner§08

Trade-offs: every choice costs something

In one sentenceThere are no free choices in system design, only trade-offs. A good design names what it gave up, and why that was the right thing to give up for these requirements.

Every box from §06 fixed a problem and added one. A cache made reads fast and made them possibly stale. A second app server added capacity and required a load balancer. This section covers the trade-offs that come up in almost every design, and how to talk about them.

Consistency vs availability vs latency

As soon as data lives in more than one place (replicas, caches, a second region), a write takes time to reach every copy. While it travels, the copies disagree. You have to pick what happens then:

  • Consistency first: every reader sees the latest write, or gets an error. To guarantee that, writes must wait until enough copies have confirmed, which costs latency, and when copies can't reach each other, some requests must be refused, which costs availability.
  • Availability first: every request gets a fast answer, from whatever copy is nearby, even if that copy is a little behind. That's Eventual consistency: the copies agree eventually, once the update arrives.

The CAP theorem is the formal version: when the network between copies breaks (a Network partition), a system must choose between staying correct and staying available. PACELC adds the everyday half: even when the network is fine, you trade latency for consistency. Both are taught properly in CAP & PACELC. Try the choice yourself:

Consistency or availability?
SnipShare runs a copy of its database in the US and in Europe. Pick what to favour, then cut the link between the regions.
The US user's save
Slower: ≈ 80–150 ms
The US write waits until the EU copy confirms it too: one round trip across the ocean on every write.
What the EU reader sees
Always the latest
Both copies agree before the write is acknowledged, so a reader anywhere sees the new snippet.
Consistency first: correct answers, paid for with slower writes and with errors when the network fails. Neither is free; the requirements decide which cost is acceptable.

Choose per piece of data, not per system

The most useful insight about this trade-off is that you don't make it once. Different data in the same product needs different answers:

SnipShare dataIf two copies briefly disagree…Choose
A snippet's contentA reader in another region sees it a second later. Nobody is harmed.Availability (and fast local reads)
View countersThe count is off by a few for a moment.Availability
A custom short name like /my-config (if we add it)Two people in two regions both get the same name. One of them loses their link.Consistency
Payment for a premium plan (if we add it)Charged twice, or upgraded without paying.Consistency (see the payments case study)

SnipShare's requirement "others may see a new snippet up to a few seconds late" is exactly a decision to favour availability and latency for snippet content. Writing that sentence down in §02 is what lets you use replicas and caches freely now.

Cost vs complexity

Every box costs twice: once in money (machines, managed services, network) and once in complexity: more to deploy, monitor and understand, more ways to fail. The second cost is usually bigger, and it never shows up on the bill.

✕ Complexity first
design for 'web scale' on day one
6 microservices, Kafka, Cassandra,
Kubernetes across 3 regions
→ peak load: 30 writes/s, 3,000 reads/s
→ a team of 4 spends most of its time
  operating infrastructure
✓ Simplest thing that works
design for the numbers
2 app servers + load balancer,
managed Postgres + standby replica,
managed Redis, object storage
(+ a CDN once far-away users need it, §04)
→ same load, lots of headroom
→ the team ships features

The simple design isn't the "cheap" option; it's the one that matches the numbers. It also leaves a clear upgrade path: each next box has a number attached that tells you when to add it.

Build vs buy

Build vs buy is the choice between running a component yourself and using a product or managed service. For SnipShare, "buy" means a managed database, cache, load balancer and object storage, which matches the "small team" requirement from §02.

Build (run it yourself) when…Buy (use a managed service or product) when…
It's your core product, the thing that makes you differentIt's plumbing every company needs: databases, queues, load balancers, auth
No product fits the requirementsA product fits well enough, and you can live with its limits
You have the people to run it and be on call for itThe team is small, or its time is better spent on features
The managed price at your scale is far above running itIts price is acceptable for the people-time it saves

The hidden cost of buying is lock-in: moving off a managed service later can be a large project. The hidden cost of building is the on-call rotation. Cloud Basics goes deeper on managed vs self-hosted.

How to state a trade-off

A trade-off said well has four parts: the choice, what it buys, what it costs, and why the cost is acceptable here. Compare:

  • ❌ "We'll use eventual consistency because it scales better."
  • ✅ "Snippet reads are served from the cache and the CDN, so a new snippet can take up to a few seconds to appear for other readers. That's allowed by the requirements, and it keeps the database free for writes. There are no accounts, so after saving, the author's browser gets a short-lived cookie; for a minute, requests with that cookie read from the database directly, so the author never sees their own snippet missing."

The second one can be checked: a reviewer can disagree with the requirement or with the "minute", instead of with a slogan. (That last trick gives the author Read-your-writes: Replication shows how to route those reads.)

Practitioner§09

Step 5: What if each box dies?

In one sentenceEvery box on the diagram will fail one day. For each one, you should be able to answer: what do users see, and how does the system recover?

So far the design handles the load. But machines crash, disks fill up, networks drop packets and deploys go wrong. A design that's only correct when everything works is half a design. The method is mechanical, which is what makes it useful: point at each box in turn and ask "what if this dies?"

Three questions per box

  1. What do users see? Nothing? Slower pages? One feature missing? The whole site down?
  2. How does it recover? Automatically (a load balancer's health check skipping a dead server, or a Failover to a standby copy), or does a person have to do something?
  3. How would we know? What metric or alert tells us before users do?

Two answers are red flags. "The whole site is down" for a box that has no spare means it's a single point of failure. And "we'd find out from users" means you're missing monitoring.

Try it

This is SnipShare's design with every block. Click (or tap) a box to kill it, and look at what happens to each of the three endpoints. Then kill two at once: the cache first, then the cache and the primary database.

What if this box dies?
Tap any box to kill it (tap again to bring it back). Kill several at once to see failures combine.
👥 browser
GET /snippets/{id}
Working.
POST /snippets
Working.
GET /search
Working.
Nothing is broken. Try the cache first, then the Postgres primary, then both together.

Patterns that came up

Redundancy: run one more than you need. The app tier survives a dead server because there are two behind a Load balancer that runs health checks. Each runs at about 30% at peak (§07); if one dies, the other carries everything at about 60%, right at our target. The rule of thumb is N+1 redundancy: enough capacity for the peak, plus one spare, so a failure (or a deploy) during the peak doesn't overload the rest. That's also why §07's "one server at 60%" wasn't good enough.

Failover: a standby that can take over. The database primary can't simply be duplicated, because two primaries accepting writes would disagree. Instead a Replica follows it and gets promoted when it dies. That's the real reason SnipShare has a replica: for failure, not for load. One catch: replication is usually an asynchronous write, so the primary says "saved" before the replica has the data, and the last few writes before a crash can be missing after promotion. For "never lose a saved snippet", the primary must wait for the replica to confirm each write. How promotion works and what can be lost is in Replication.

Know what your CDN is hiding. If the CDN fails completely, every read it used to answer lands on your own servers, which CDN people call the origin. Plan for the origin to survive that, or for a second CDN.

Watch for load-bearing caches. Losing the cache didn't break anything by itself, but it multiplied the database's load by 10. If the database can't survive that, the cache has quietly become something the system can't live without. Either size the database for a cold cache, or protect it, for example with request coalescing: when many requests miss on the same key at once, only one goes to the database and the rest wait for its answer. Caching explains it.

Graceful degradation: let optional features fail alone. Search being down shouldn't stop people reading snippets. Keep non-essential features off the critical path: call them with short timeouts, and show "unavailable right now" instead of an error page. Graceful degradation is a design decision you make in advance, not something that happens by luck.

Decouple slow work with a queue. Indexing a snippet for search happens in a background worker fed by a queue. When the worker dies, the queue holds the backlog and nothing else breaks. The cost: you must decide what happens if the queue itself is down, and handle jobs that run twice (Idempotency covers that). A common answer is the outbox pattern: save the snippet and an "index this" row in the same database transaction, and let a small relay move rows from that table to the queue. Saving never depends on the queue being up, and no job is lost (Sagas: outbox and inbox).

Correlated failures

The toggles above kill one box at a time, but real outages often take several at once: a whole data center (an Availability zone) loses power, a bad config is deployed everywhere, a shared dependency like DNS breaks. Two replicas on the same rack aren't really two. That's why production designs spread copies across zones, and roll out changes gradually. Reliability & SRE covers planning for losing a whole zone.

Staff-level§10

SLOs as requirements, and capacity with headroom

In one sentenceIn production, non-functional requirements become SLOs: numbers you measure every day. And capacity isn't "enough for today": it's enough for next year's peak, with a zone down.

§02 wrote "read p99 < 200 ms" and "99.9% available" as requirements. At a staff level, those sentences have to become things you can measure, alert on, and use to decide what to build next. This section connects the design to the operational topics: Observability and Reliability & SRE.

From requirement to SLO

An SLI is a measured number that reflects what users experience. An SLO is the target for it over a time window. The gap between 100% and the target is the Error budget: how much failure you're allowed before reliability work takes priority over features.

Requirement (§02)SLI (what we measure)SLO (the target, 30-day window)
Opening a snippet feels fastShare of GET /snippets/{id} answered in under 200 ms99%
The site is upShare of requests answered without a server error99.9%
Saved snippets are never lostShare of saved snippets readable afterwards (checked by a background prober)100%, with any miss treated as an incident
New snippets appear in searchShare of snippets searchable within 5 minutes of saving99%

Notice the last one: it measures the queue + indexer path from §09. If you only measure what the app server does, an indexer that has been dead for a day looks perfectly healthy. An SLO per user-visible promise catches failures in the background parts too.

What the nines mean

"Nines" are shorthand for availability targets. Over a 30-day month (43,200 minutes), each one allows this much failure:

TargetAllowed failure per 30 daysRoughly
99%432 minutes7 hours
99.9%43.2 minutesunder an hour
99.99%4.3 minutesa single slow failover
99.999%26 secondsno human can even react in time

Each nine allows 10× less downtime and typically costs far more than 10× the effort: automatic failover everywhere, multiple zones or regions, very careful deploys. That's why "how many nines?" is a requirements question with a cost attached, not a technical detail.

Availability multiplies along the request path

If a request needs three boxes in a row and each is available 99.95% of the time or better, the request succeeds only when all three are up, so you multiply. A worked example with round, made-up numbers:

  • Load balancer 99.99% × app tier 99.95% × database 99.95%
  • = 0.9999 × 0.9995 × 0.9995
  • ≈ 0.9989, which is 99.89%: below a 99.9% target, even though every part beats it.

Every dependency on the critical path lowers the ceiling. That's another argument for the simple design: fewer boxes in a row means fewer numbers to multiply. It also explains graceful degradation from §09: search isn't on the read path, so its downtime doesn't count against reading.

Redundancy works the other way. Two copies that fail independently, each up 99% of the time, are both down only 0.01 × 0.01 = 0.01% of the time: 99.99% together. The catch is the word "independently": two copies in the same zone, running the same buggy config, fail together. That's why correlated failure (§09) is the thing to design against.

Capacity planning with headroom

Capacity planning asks: will we have enough of everything for the busiest moment coming up, even if something breaks? Here's the app tier for SnipShare, step by step. Unlike the simulator, these inputs should be measured, with a load test, not guessed:

Why 15 here when §07 ended with 2? §07 used the simulator's made-up 5,000 QPS per server and only today's load. Here a real load test says 1,000 QPS per server, we keep 40% spare, plan for next year, and survive losing a zone. Same method, honest inputs, very different answer: that's why capacity planning uses measurements, not guesses.

  1. Today's peak: about 3,500 QPS (the exact figure from the §03 script).
  2. Growth: the product team expects +50% over the next year → forecast peak 3,500 × 1.5 = 5,250 QPS.
  3. Per-server capacity: a load test shows one app server handles about 1,000 QPS before p99 latency breaks the SLO.
  4. Target utilization: plan to run at most 60% busy at peak, which leaves Headroom for spikes and slow moments → each server counts as 600 QPS.
  5. Survive losing a zone: servers are spread across 3 zones; if one dies, the other two (2/3 of the fleet) must carry the whole peak → 5,250 ÷ (600 × 2/3) ≈ 13.1 → round up to 5 per zone = 15 servers.
servers.go
package main

import (
	"fmt"
	"math"
)

// serversNeeded: enough servers that, even with one zone down,
// the rest run at or below targetUtil at the forecast peak.
func serversNeeded(peakQPS, perServer, targetUtil float64, zones int) int {
	healthy := float64(zones-1) / float64(zones) // share left after losing one zone
	n := peakQPS / (perServer * targetUtil * healthy)
	perZone := math.Ceil(n / float64(zones)) // same count in every zone
	return int(perZone) * zones
}

func main() {
	forecast := 3500 * 1.5 // today's peak × expected growth over the next year
	fmt.Println("forecast peak:", forecast, "QPS")
	fmt.Println("servers:", serversNeeded(forecast, 1000, 0.6, 3))
}
output (both programs)
forecast peak: 5250 QPS
servers: 15

Fifteen servers, when today's peak could be carried by 4 servers running at nearly 90% busy, can look wasteful. It isn't: it's the price of "a whole zone can die on our busiest day of the year and users don't notice". The same reasoning applies to everything else with a limit: database connections, cache memory, disk, queue throughput, and cloud quotas. Reliability & SRE covers forecasting and the limits that aren't servers.

Staff-level§11

Step 6: Write it down in a design doc

In one sentenceA design that only lives in your head can't be reviewed. A design doc turns the method into something others can challenge, before anyone has written code that's expensive to throw away.

At work, the output of the method isn't a whiteboard photo: it's a design doc (some teams call it an RFC, "request for comments", because it's circulated for others to comment on). It records the requirements, the numbers, the design and, most importantly, the reasoning, so reviewers can disagree with a specific assumption instead of with the whole thing. Six months later it also answers "why on earth did we build it this way?".

A one-page template

Every section maps to a step of the method. Short is better: a reviewer should get the main idea in five minutes.

design-doc-template.md
# SnipShare v1: paste a snippet, share a link
Author · Reviewers · Status (draft / in review / approved) · Date

## Context and goals        ← why now, what problem, for whom
## Requirements             ← §02: functional, non-functional (as SLOs), out of scope
## Estimates                ← §03: QPS avg/peak, storage/year, bandwidth, read:write
## API and data model       ← §05: endpoints with load, schema, access patterns
## Proposed design          ← §06–07: diagram, request path, why each box exists
## Alternatives considered  ← what else you could do, and why not
## Failure modes            ← §09: what if each box dies; what we monitor
## Rollout and migration    ← how it gets to production safely; how to undo it
## Risks and open questions ← what you don't know yet, and how you'll find out

SnipShare v1, filled in

Here's the whole method's output for SnipShare in one place: one or two lines per heading, pulling together §02–§10.

snipshare-v1.md
# SnipShare v1: paste a snippet, share a link
Context    Developers want to share code without accounts. Goal: launch v1 for ~10M daily users.
Reqs       Paste ≤ 1 MB → short link; read by link; expiry; keyword search on public snippets.
           Read p99 < 200 ms, write p99 < 1 s, 99.9% available, saved snippets never lost,
           others may see new snippets a few s late. Out of scope: accounts, comments, edits.
Estimates  ≈ 3,000 read QPS + 30 write QPS at peak (100:1, read-heavy); ≈ 4 TB bodies/yr
           (≈ 60 TB over 5 yr with 3 copies); ≈ 36 GB metadata/yr; ≈ 30 MB/s out at peak.
API/data   POST /snippets, GET /snippets/{id} (97% of traffic), GET /search.
           Postgres row per snippet (~100 B) + body in object storage under body_key.
Design     LB → app servers → Redis cache (90% hits) → Postgres primary + standby replica;
           bodies in object storage; on save, an outbox row → queue → indexer → search index.
           Every box under 60% at peak. App tier: 2 servers on paper (§07); sized from a
           load test for launch: ≈ 15 across 3 zones for next year's peak (§10).
Alternatives  Bodies in Postgres (backups too big); DynamoDB (no need to switch);
           replicas instead of a cache (a cache is cheaper for unchanging snippets).
Failures   App server or a whole zone dies → the rest stay under target (N+1). Primary dies → promote replica
           (writes wait for the replica, so nothing saved is lost). Cache dies → DB must
           survive 10× reads. Search/indexer down → search degraded, core unaffected.
SLOs       99% of reads < 200 ms; 99.9% without server errors; 99% searchable within 5 min.
Rollout    Launch behind a feature flag; load test to 2× forecast peak first.
Risks      Real per-server capacity unknown until load tested (§10 sizing depends on it);
           short-id collisions across servers (see the URL shortener case study).

The section reviewers read first

Experienced reviewers often jump straight to Alternatives considered. A design with no alternatives suggests the author picked the first idea that came to mind. Two or three real options, each with one line on why not, show the trade-offs were weighed:

OptionWhy not (for SnipShare v1)
Store snippet bodies in Postgres tooSimplest, but ≈ 4 TB/year would make backups and replicas slow and expensive. Revisit if snippets stay small.
A key-value store (e.g. DynamoDB) instead of PostgresFits the access pattern well, but the team already runs Postgres; nothing in the numbers requires the switch.
No cache; add read replicas insteadWorks, but snippets never change and the same ones are read repeatedly: a cache is cheaper per read.

"Do nothing" or "the simplest possible version" belongs in the list too. Sometimes it wins.

Running a design review

  • Send the doc ahead. Reviewers comment in writing first; the meeting is for the disagreements that remain.
  • Ask reviewers for the questions that hurt. "Which number would make this design wrong?" "What happens on the busiest day if the primary dies?" "What's the plan to undo this?"
  • Record the decisions in the doc, including the ones you rejected. That's the document's long-term value.

The second-system trap

The second-system effect is the urge, when replacing a system that worked, to put every improvement you ever wanted into the replacement at once. Fred Brooks described it in The Mythical Man-Month (1975), and it still happens: the "v2 rewrite" that is general enough for every future need, takes three times as long as planned, and ships with more problems than v1 had.

Warning signs in a design doc:

  • Requirements that no user asked for ("pluggable storage backends", "support for any future content type").
  • Capacity planned for 100× today's numbers with no forecast behind it.
  • More new components than problems listed in "Context and goals".
  • No step-by-step migration: the new system replaces the old one in a single big switch.

The fix is the same as in §07: solve the problems the numbers and requirements show, and leave a clear path for the rest. Migrate in small steps, keeping the old and new systems side by side, so each step can be checked and undone. (The strangler fig pattern is the classic way to do this.)

Practitioner§12

Common mistakes

In one sentenceAlmost every bad design skips a step of the loop. Here are the steps that get skipped most.
01

Choosing tools before knowing the problem

"We'll use Kafka, Cassandra and Kubernetes" in the first minute. Tools are answers; without requirements and numbers there's no question yet. Fix: requirements and estimates first (§02–03). Name a product only when a box needs one, and say why that product.

02

Designing for the average

1 million requests a day "is only 12 QPS", until the morning peak brings 5× that, and a launch day brings 50×. Fix: always multiply by a peak factor, and ask whether the product has bursty events (§03).

03

Boxes without numbers

A diagram with a cache, three replicas and a queue, and no idea how much load any of them carries. Fix: write the load next to each box (§05, §07). If you can't say which number justifies a box, remove it.

04

Fixing the wrong bottleneck

Adding read replicas to a database that is overloaded by writes, or adding app servers when the database is the limit. Fix: find the box with the highest utilization first, and check why it's busy (reads or writes? repeated or varied?) before choosing the fix (§07).

05

Treating precision as accuracy

Spending ten minutes computing "11,574.07 QPS" from inputs that were guesses to begin with. Fix: round to one significant digit and powers of ten. Being within 10× is the goal; spend the time on the design instead (§03).

06

Only designing the happy path

The design works perfectly as long as nothing breaks. Fix: walk every box with "what if it dies?" (§09). Look especially for single points of failure and for caches the database can't live without.

07

One consistency answer for everything

Either "everything must be strongly consistent" (slow and fragile) or "eventual consistency everywhere" (until two people get the same username). Fix: decide per piece of data, and write the decision down (§08).

08

Forgetting the network

Ten calls in a row to other services, each a few milliseconds, and a page that somehow takes a second. Or servers on one continent serving users on another. Fix: count round trips on the request path and spend the latency budget deliberately (§04).

09

Building version 3 first

Planning for 100× growth with no forecast behind it, or rewriting a working system with every wished-for feature at once. Fix: build for the numbers you have, plus a clear plan for the next step up (§07, §11).

Practitioner§13

Check yourself

Work each answer out on paper before you click: a quick estimate is the skill being tested. Q1, Q2 and Q6 practise §03; Q3 is §06–07; Q4 is §04; Q5 is §09; Q7 is §02.

QUESTION 1 / 7
Requests per day → per second

A service gets 50 million requests a day, spread evenly. Roughly what's the average QPS?

QUESTION 2 / 7
Average vs peak

Average traffic is 2,000 QPS with a peak factor of 3. One app server can take 1,000 QPS while staying under the 60% target (that already includes the headroom). How many servers should you run?

QUESTION 3 / 7
Replicas and writes

Your database is at 100% because of writes (read:write is about 1:1). You add three read replicas. What happens?

QUESTION 4 / 7
Spending the latency budget

A page must load within 200 ms. The server makes 3 database queries in the same data center (≈ 1 ms each), then calls a service on another continent twice, one call after the other (≈ 120 ms each). Does it fit?

QUESTION 5 / 7
When the cache dies

A cache in front of the database has a 90% hit rate, and the database currently serves 500 reads a second. The cache crashes and restarts empty. What does the database see?

QUESTION 6 / 7
Storage per year

Users upload 2 million photos a day, about 500 KB each. Roughly how much new storage per year, before copies?

QUESTION 7 / 7
Where to start

You have 45 minutes to design a ticket-booking site. Which is the best first move?

Answered 0 / 7
Foundations§14

Cheat sheet

📝
1. RequirementsFunctional (what it does), non-functional (latency, availability, durability, consistency, cost) as numbers, and a written out-of-scope list.
🧮
2. EstimateRequests/day = users × actions. ÷ 105 → QPS. × peak factor (2–10). Storage = writes/day × size × 365 × years × copies.
🔌
3. API + data modelList the endpoints with their load. List the access patterns. They pick the database and show which call to optimise.
🔥
4. Bottleneck loopStart with one server and one database. Find the busiest box, change one thing, recompute. Stop when everything has headroom.
💥
5. FailureFor every box: what do users see, how does it recover, how would we know? Kill SPOFs with N+1 and failover.
📄
6. Write it downA short design doc with alternatives considered. Name the number behind each box.
NumberApproximate value
Seconds in a day86,400 ≈ 105
1 million / day≈ 10 QPS (exactly 11.6)
100 million / day≈ 1,000 QPS
1 KB / MB / GB / TB / PB103 / 106 / 109 / 1012 / 1015 bytes
RAM read≈ 100 ns
SSD random read≈ 20–100 µs
Round trip, same data center≈ 0.5 ms
Round trip, across a continent or ocean≈ 80–150 ms
99.9% / 99.99% availability≈ 43 min / ≈ 4.3 min of failure per 30 days
Bytes/s → bits/s× 8
SymptomFirst reach for
One app server fullLoad balancer + more app servers
Database busy with repeated readsCache
Database busy with varied readsRead replicas
Database busy with writes, or data too bigShards (replicas won't help)
Far-away users, popular public contentCDN
Slow work the user needn't wait forQueue + workers
Big filesObject storage
Keyword searchSearch index
Strong consistency neededOnly for the data that really needs it
Finished reading?Mark it complete to track your progress on a roadmap. Saved in this browser only.
Up next in System DesignDesign a URL Shortener
Read →