“Design Twitter.” Forty-five minutes, a whiteboard, and someone watching. It is the interview format most engineers dread, and the reason is not that the material is hard — it is that the question has no answer. There is no test to run and nothing to compile. You are being asked to have a conversation, and most people have not been told what the conversation is for.
This post is about what that conversation actually is, how to run it so that an hour goes somewhere, and what the rest of this track covers.
What system design is
System design is choosing how to arrange the parts of a running system so that it does what it needs to do at the size it needs to do it, and keeps doing so when something breaks.
That definition is doing more work than it looks. Three phrases in it are the whole subject:
- “the parts” — there are perhaps a dozen, and they are the same dozen every time. Load balancers, caches, databases, replicas, queues, object stores, CDNs, search indexes. The next post is a tour of all of them.
- “at the size it needs to” — a design for a thousand users and a design for a hundred million are different designs, and neither is better. Picking the wrong one in either direction is the most common mistake there is.
- “when something breaks” — not if. At any real scale something is always broken. A design that only describes the happy path is a sketch, not a design.
What it is not is architecture astronomy. Every real decision here is a trade: you give up consistency to get availability, spend memory to save latency, accept complexity to get throughput. An answer that has no cost attached is an answer that has not been thought through.
Why the question has no right answer
Ask five senior engineers to design a URL shortener and you will get five different systems, and three of them will be fine. This frustrates people who come from algorithm interviews, where there is a correct output and a complexity class.
The interviewer is not comparing your design to a model answer. They are watching for something much simpler: can you make a decision, say why, and know what it costs?
Concretely, the things being scored:
| What they watch for | What it looks like when it is missing |
|---|---|
| Do you ask what you are building before building it? | Boxes on the board in the first two minutes |
| Can you estimate? | “We’ll need a lot of servers” |
| Do you know the standard parts and what each is for? | A cache added because caches are good |
| Can you name the trade you just made? | Every choice presented as obviously correct |
| Do you know where it breaks? | A design with no failure modes discussed |
| Can you go deep when asked? | Re-explaining the diagram instead of the mechanism |
The last row is the one that separates candidates who have read about system design from candidates who have built systems. Anyone can draw the boxes. The question “what happens if two people book the last room at the same instant?” is where the diagram stops helping.
The four steps
An hour with no structure becomes forty-five minutes on the part you happen to find interesting and no time for the rest. This is the structure. It is not a secret — interviewers use it too, and several will tell you so.
1. SCOPE 5-10 min what are we building, and what are we not?
| functional + non-functional requirements
v
2. ESTIMATE 5 min how big is it? QPS, storage, bandwidth
| the numbers that decide the design
v
3. HIGH LEVEL 15-20 min the boxes, the arrows, the API, the schema
| one diagram both people can point at
v
4. DEEP DIVE 15-20 min one hard part, properly
usually the one THEY pickStep 1 — Scope it
“Design Twitter” is not a specification, it is an opening. Nobody can design Twitter in an hour; you can design a piece of it, and the first job is agreeing which piece.
Split what you ask into two kinds. Functional requirements are what it does: can users follow each other? Are there images? Does the timeline have to be chronological? Non-functional requirements are what it has to be like while doing it: how many users, read-heavy or write-heavy, how fresh must data be, how bad is downtime?
The non-functional list is the one that decides the design, and it is the one candidates skip. “Can a user see their own post immediately?” sounds like a detail and is the difference between a system that can use replicas freely and one that cannot.
Written down, a scoped answer to “design Twitter” looks like this — and writing it in the corner of the board is worth doing, because you will refer back to it:
IN SCOPE OUT OF SCOPE (say these out loud)
post a tweet (text only) images and video
follow / unfollow direct messages
home timeline search
~ trending, ads, notifications
NON-FUNCTIONAL
300M daily active users
read:write ~ 100:1 <- this one decides the design
timeline may be seconds stale <- so may replicas and caches
a user must see their OWN tweet
immediately <- read-your-writes: NOT freely cacheable
availability > consistency <- a missing tweet beats an error pageNotice how much of the design is already fixed by four lines. A hundred-to-one read ratio says every serious effort goes into the read path. “Seconds stale is fine” unlocks replicas and caching. “Their own tweet immediately” puts one specific exception back, and that exception is a real piece of engineering — the consistency post covers it under read-your-writes.
Three or four minutes of this is enough. You are not gathering requirements for real, you are establishing that you know they exist and picking a scope you can finish.
Step 2 — Estimate
Numbers turn opinions into decisions. “We should cache this” is an opinion. “This is 40,000 reads a second against 300 writes, so the read path is the entire problem” is a decision, and it makes the next twenty minutes obvious.
You need surprisingly little arithmetic. Daily active users, actions per user per day, the read-to-write ratio, the size of one record. From those: requests per second, storage per year, bandwidth. The estimation post works several of these through end to end.
The point is not precision. Being out by a factor of three changes nothing; being out by a factor of a thousand means you are designing the wrong system, and the arithmetic is how you find out which one you are doing.
Step 3 — High-level design
Now the boxes. Start with the smallest thing that satisfies the requirements, and say out loud that it is the smallest thing — a client, a server, a database. Then grow it where the numbers from step 2 say it has to grow.
Growing it deliberately is what distinguishes this from drawing a diagram from memory. Every box should arrive because something forced it:
"40k reads/sec" -> the database cannot serve that -> cache, read replicas
"images" -> they do not belong in a row -> object store + CDN
"email on signup" -> not on the request path -> queue + worker
"one box is a SPOF" -> more than one of it -> load balancer, stateless tierTwo things belong here that people leave out. The API — four or five endpoints with their inputs and outputs — because it forces you to be concrete about what the system does:
POST /tweets {text} -> {id, createdAt}
GET /timeline?cursor= - -> {tweets[], nextCursor}
POST /users/{id}/follow - -> 204
DELETE /users/{id}/follow - -> 204Four lines, and two design decisions are already visible in them. The timeline is paginated by cursor rather than by page number, because offset pagination over a feed that is constantly growing shows people duplicates and skips others. And follow is a PUT-like pair rather than a toggle, so a retried request cannot accidentally unfollow — the same idempotency argument the concurrency post makes at length.
And the data model, because the schema is where most designs are actually decided. A design that never says what a row looks like has not committed to anything:
CREATE TABLE tweets (
id BIGINT PRIMARY KEY, -- sortable by time; see the unique-id post
user_id BIGINT NOT NULL,
text VARCHAR(280) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE follows (
follower_id BIGINT NOT NULL,
followee_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (follower_id, followee_id)
);
-- The index that decides whether a timeline query is possible at all.
CREATE INDEX idx_tweets_user_time ON tweets (user_id, created_at DESC);That last index is not decoration. Building a timeline means “the most recent tweets from the people I follow”, and without an index in exactly that shape the query sorts the table every time. Whether you can serve timelines from this schema at all, or whether you need to precompute each user’s feed on write, is the deep dive this question always goes to.
Step 4 — Deep dive
The interviewer picks a thread and pulls it. “How does search work?” “What happens when this instance dies mid-write?” “How do you stop double-booking?”
This is the part that is actually being assessed, and it is why the earlier steps are kept short. The first three steps establish that you can structure a problem; this one establishes whether you have ever solved one. It is the reason most of this track is mechanisms rather than diagrams — caching, locking, queues, consistency — because the deep dive is always about a mechanism.
Here is what “going deep” actually means, on a question every booking system gets asked: two guests book the last room at the same instant — what happens?
The shallow answer is “check whether it is available, then write the booking.” It is also the answer almost everyone gives first, and it is wrong in a way that is invisible until you draw the timeline:
time ->
guest A: check "is 5-7 Jan free?" ──> YES ─────────> INSERT booking ✓
guest B: check "is 5-7 Jan free?" ──> YES ─────────> INSERT booking ✓
▲
both checks ran before either insert │
landed, so both saw an empty calendarNothing is wrong with either request. The gap between the check and the write is the bug, and no amount of care in application code closes it — a lock in one process does not stop the second process, and both may be on different machines.
The answer is to make the overlap impossible in the only place that can see both writes at once, which is the database:
ALTER TABLE bookings ADD CONSTRAINT bookings_no_overlapping_bookings
EXCLUDE USING gist (
property_id WITH =,
daterange(check_in, check_out, '[)') WITH &&
) WHERE (status IN ('PENDING', 'CONFIRMED', 'COMPLETED'));Now the second insert fails, whatever the application believed a millisecond earlier, and the application’s job changes from preventing the race to translating its outcome into a decent error message. That is a deep dive: a wrong answer, why it is wrong, the mechanism that fixes it, and what the fix costs. The concurrency post takes it further.
The vocabulary the numbers come in
Four terms get used constantly and get mixed up almost as often. They are worth pinning down once, because a requirement stated in the wrong one leads to the wrong design.
Latency is how long one request takes. Throughput is how many you handle per second. They are not the same axis and improving one often costs the other — batching writes raises throughput and raises latency at the same time.
Latency is also never a single number. Quote it as percentiles:
p50 = 40ms half of requests are faster than this
p95 = 180ms the slow ones people notice
p99 = 900ms 1 in 100 -- and on a page making 20 calls,
~18% of page loads contain one of these
p999 = 4s the ones that generate support ticketsAn average hides all of it. A system with a 40ms mean can have a p99 of two seconds, and the p99 is what your users are describing when they say the site is slow.
Availability is the fraction of time the system works, and it is quoted in nines. The step between them is worth knowing by heart because each one is roughly ten times more expensive than the last:
| Availability | Downtime per year | Per month | What it takes |
|---|---|---|---|
| 99% | 3.65 days | 7.2 hours | one server, someone on call |
| 99.9% | 8.8 hours | 43 minutes | redundancy, health checks, fast rollback |
| 99.99% | 52 minutes | 4.3 minutes | multi-AZ, automated failover, no manual steps |
| 99.999% | 5.3 minutes | 26 seconds | multi-region, and it dominates every other decision |
Five nines leaves no room for a human to notice, decide and act — twenty-six seconds a month is less than it takes to read a page. That is why it is not a target you pick, it is one that reshapes the entire system, and why claiming it casually in an interview invites a question you may not want.
Finally consistency: whether everyone reading right now sees the same thing. It is the one that trades directly against availability, and it gets a post of its own.
What it looks like when it goes well
A compressed version, on a question this track answers properly later. “Design a URL shortener.”
Scope. Shorten a URL, redirect a short code, and it must never send two people to different places for the same code. No user accounts, no analytics dashboard — say so, and say you would add them if there were time. Ask: how long do links live? Custom aliases?
Estimate. Say 100 million new links a day. That is roughly 1,200 writes a second. Redirects run maybe 10:1 against creates, so 12,000 reads a second. A row is about 500 bytes, so 50GB a day, 18TB a year.
Those three numbers have already designed the system. 1,200 writes a second is comfortable for one well-indexed database. 12,000 reads a second is not, but redirects are perfectly cacheable, so a cache absorbs them. 18TB a year says storage grows forever and something has to expire or shard eventually.
High level. Two paths, and they are wildly asymmetric:
WRITE (1.2k/s) READ (12k/s)
POST /urls GET /{code}
| |
v v
app server app server
| |
| generate code |-- cache hit? --> 301/302, done
v | (~95% of them)
database <-------------------------- miss: one indexed lookup
code -> long_url then populate the cacheDeep dive. “How do you generate the code?” — and now you are in the real question: a counter encoded in base62 gives you uniqueness for free but leaks how many links exist and lets anyone enumerate them; hashing the URL and truncating gives you neither problem but does give you collisions. That trade, stated with both costs, is the answer.
Designs grow; they are not chosen
The single most useful habit is refusing to start from the finished picture. Start from one server and let each requirement push you.
[1] browser ──> server ──> database works to ~10k users
[2] browser ──> LB ──> server x N ──> database one box was a SPOF
│ (needs the tier to be stateless)
[3] ├──> cache reads were hitting the DB
v
database ──> replica reads still were
[4] browser ──> CDN ──> LB ──> server x N static files never needed
│ to reach a server at all
├──> cache
├──> queue ──> worker slow work left the
v request path
primary ──> replicasEach step is one requirement being satisfied, and you can say which. That is a design. The same four boxes drawn from memory in one go is a diagram, and the difference is audible to whoever is listening.
It also stops the second-most-common failure, which is arriving at step 4 for a system that needs step 1. If the numbers say 200 requests a second, the honest answer is one server and a database, and saying so with the arithmetic behind it is a stronger answer than a queue nobody needs.
The mistakes that sink people who know the material
Almost none of these are knowledge gaps.
- Drawing before asking. The most common one, and it costs the whole interview, because the design is now aimed at a problem nobody agreed on.
- Designing for Google. Sharding, multi-region replication and a consensus protocol for a system with 10,000 users. Over-engineering reads as inexperience, not ambition — knowing when not to shard is the more valuable signal.
- Naming technologies instead of mechanisms. “I’d use Kafka” answers nothing. “I need writes to survive a consumer being down for an hour, so I want a durable log I can replay rather than a queue that drops what it delivers” is the answer, and it happens to be Kafka.
- Going silent. Thinking quietly for two minutes looks identical to being stuck. Narrate the options, including the ones you reject.
- Defending instead of listening. “What if that node fails?” is not an attack, it is a hint that you have missed something. The right response is to think about it, not to explain why it will not happen.
- Never mentioning a downside. If every choice sounded free, you have either not understood the choices or you are hiding them. Say the cost yourself, before you are asked.
How this track is arranged
Eighteen posts in three groups, and they are meant to be read in order.
The parts and how they scale — the vocabulary of every answer. What a production system is made of, how to estimate, load balancing, caching, database scaling, consistency and CAP, message queues, concurrency and locking, rate limiting, and generating unique ids. These are the mechanisms the deep dive always lands on.
The case studies — whole systems, worked end to end. A URL shortener, a chat system, a notification system, then Airbnb, Amazon and an airline booking system. Each one assembles the parts from the first group.
The interview post — the questions you should be able to answer out loud, each linked back to the post that works it through.
Where the examples come from
One thing separates this track from most writing on the subject: where it can, it shows the mechanism running rather than describing it.
Most of the code is taken from StayHub, a working Airbnb-style booking application — FastAPI and Postgres for writes, Elasticsearch for search, Redis for caching and rate limiting, and a transactional outbox for asynchronous work. It is small, but it is real: it has a test suite, it runs, and every number quoted in this track was measured on it.
┌── writes ──> FastAPI ──> Postgres
React apps ───┤ │
├── reads ──> Hasura ─────────┘
└── search ──> FastAPI ──> Elasticsearch
▲
sunk in application code from every write pathThat split is itself a design decision with a cost, and the consistency post is largely about the cost: two datastores cannot be updated atomically, so the search index is sometimes wrong, and the interesting question is for how long and what you do about it.
Its application entry point, so the later posts have something concrete to refer back to:
app = FastAPI(
title=settings.app_name,
version="1.0.0",
...
lifespan=lifespan,
docs_url="/docs",
)
app.add_middleware(RequestContextMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
register_exception_handlers(app)
app.include_router(api_router, prefix=settings.api_v1_prefix)Where StayHub genuinely cannot help — nobody is running Amazon’s catalog or an airline’s inventory on a laptop — the posts say so, and show schemas and sketches rather than pretending. A system design post that presents invented code as if it came out of a repository is worse than one that admits the sketch, because the case studies are exactly the posts people trust most.
Start here
If you read nothing else, read the framework above and the parts list that follows this post. Between them they cover the first twenty minutes of every system design interview you will ever have.
Then work through the mechanisms. The deep dive is where these interviews are won, and the deep dive is always about how one specific thing works.