Design Twitter / News Feed
Our first real interview. We'll run it like one: clarify the prompt with the interviewer, converge on requirements, do the actual math, then design — starting from the obvious approach and letting each failure push us to the next idea. By the end you'll have rebuilt Twitter's timeline the way it's really discovered, not memorized a diagram.
The prompt — and don't panic
The interviewer says: “Design Twitter.” Two words, a blank whiteboard, forty-five minutes. The instinct is to start drawing boxes. Resist it. The first thing a strong candidate does is recognize the shape of the problem and set a plan. This is a read-heavy feed archetype, so out loud: “Let me clarify the scope first, then size it, then design toward the read path — I expect the interesting decision to be how we assemble each timeline.” You've just told the interviewer you've seen this shape before.
Clarify before you build
You never design the system in your head — you design the one the interviewer actually has in mind. So ask. Each question below narrows the problem; click to see how a typical interviewer answers and what that pins down. Notice these aren't trivia — every answer removes a whole branch of work.
Converge on the requirements
Now turn that conversation into an explicit spec, said back to the interviewer so you both agree before any design. Separating functional from non-functional matters: the functional list bounds what you build, and the non-functional list is what you'll keep returning to in the deep dive.
Size it with real numbers
Now do the math out loud — but only the numbers that change a decision. The calculator below starts from the assumptions we pinned. Each line shows the actual arithmetic and ends in a verdict. Try editing the assumptions: notice that nothing changes the headline conclusion — this is a read-dominated system.
The data, and the one request that matters
With the scale in hand, name the data. It's small. There are users; there are tweets, each with one author and a timestamp; and there are follow edges — directed, from a follower to the followed. Notice what's missing: the timeline itself. A timeline isn't stored, it's derived from tweets plus the follow graph — and how we derive it is the whole problem.
The API is just as small: post a tweet, follow a user, and — the call the numbers above told us runs tens of thousands of times a second — GET /feed. Everything we build exists to make that one read fast. Writing the contracts out keeps us honest about what each call carries — note the feed comes back a page at a time (you can't hand someone a million tweets at once), and media isn't crammed into the tweet itself, just linked.
Start with the obvious design
Before reaching for anything clever, build the simplest thing that could possibly work — a single relational database with three tables. This is where you should always start: it's correct, it's easy to reason about, and it shows the interviewer you don't over-engineer.
We need somewhere to keep users, somewhere to keep tweets, and somewhere to record who follows whom. That last one — the follow graph — is just a table of edges: one row per “follower → followee” relationship. Here's the schema and how the tables relate:
Why a relational database for this? Users, tweets, and follows are highly structured and joined by IDs — exactly the shape SQL is built for. We get joins to walk the follow graph and secondary indexes (like one on (author_id, created_at), to find an author's recent tweets without scanning) for free. For a system whose data fits this neatly and whose correctness matters, a relational store is the obvious, boring, correct starting point.
Now, how do we build Bob's timeline? The natural query: find everyone Bob follows (a lookup in follows), grab their recent tweets (a lookup in tweets by author_id), sort by time, take the top page. In one statement:
-- Bob's home timeline = recent tweets from everyone Bob follows SELECT t.* FROM tweets t WHERE t.author_id IN (SELECT followee_id FROM follows WHERE follower_id = 'bob') ORDER BY t.created_at DESC LIMIT 20;
That's it. It's correct, and at a few hundred users it returns in a millisecond. This is the approach we'll call “build on read” — the timeline is assembled fresh, on the read, every single time. The diagram below traces what that one query actually makes the server do: fan out to everyone Bob follows, fetch their tweets, then merge and sort.
…and watch exactly where it breaks
That one query hides a lot of work. Let's trace what it costs at our numbers — ~20,000 feed reads per second, users who follow hundreds or thousands of accounts, and a tweets table growing by 100 million rows a day. Four separate things break, and each one maps to a tool from Part 1.
1. The tweets table is huge → it needs an index. Without an index on author_id, finding one author's tweets means scanning the entire tweets table — a full table scan over billions of rows (this is exactly the pain from the Indexes chapter). We'd add an index on (author_id, created_at) so the database jumps straight to an author's recent tweets instead of scanning. That's the first fix — and it's mandatory — but it isn't enough.
2. Each feed read does hundreds of index lookups + a big sort. Even with the index, a user who follows 1,000 people forces the database to pull recent tweets from 1,000 authors and merge-sort them to find the latest 20. That's a lot of work for one feed open — and it happens ~20,000 times a second. This is the “fan-out on read” cost the diagram above traced.
3. The read volume crushes one database → cache + replicas. ~20,000 heavy reads per second is far past what a single primary can serve. From the Caching and Read Replicas chapters, the instinct is right: put Redis in front and add read replicas. And we will — but notice the catch: the timeline is different for every user and changes every second, so there's no single hot value to cache like there was for a popular tweet. Caching the raw tweets helps a little; it doesn't make the expensive per-user merge go away.
4. The data outgrows one machine → sharding. At ~40 TB/year the tweets table must be sharded (the Database Sharding chapter). But sharding makes the timeline query worse: the 1,000 authors Bob follows are now scattered across many shards, so one feed read becomes a scatter-gather across shards plus a merge. The naive design fights sharding instead of working with it.
The insight: do the work once, ahead of time
Here's the mental shift. We're recomputing each timeline from scratch on every read, even though it barely changed since the last read. What if, instead, each user simply had their timeline already built — sitting in a fast cache, ready to hand back the instant they ask? Then a feed read is just one lookup. No querying a thousand people. No merging. One read.
But a prepared timeline has to be filled in somehow. The trick is to do it at write time. When Alice posts a tweet, we immediately push a copy of it into the prepared timeline of every one of her followers. By the time Bob opens the app, the tweet is already waiting in his feed. We've moved the work from the read (which happens constantly) to the write (which is rare). Given our hundred-to-one ratio, that's a fantastic trade.
This is fan-out on write. The diagram below shows the swap, top to bottom: when Alice posts, the tweet is copied into each follower's feed at write time — so when Bob later opens the app, his read is a single instant lookup. The cost moved sides.
The new wall: what about celebrities?
Fan-out on write feels like a clean win — until you think about who's tweeting. For an ordinary user with a few hundred followers, pushing a tweet into a few hundred feeds is nothing. But what happens when someone with a hundred million followers tweets? That single tweet now becomes a hundred million writes — the diagram below shows that explosion.
So neither pure approach works. Build-on-read is too slow for everyone. Build-on-write is perfect for normal users but catastrophic for celebrities. When two strategies each fail at opposite ends, the answer is usually to use both — each where it's strong.
Putting the high-level design together
Now the architecture almost draws itself — each box exists to serve a decision we reasoned our way to, and every store is something we already taught, not a new black box:
- Tweet Store — the source of truth for tweets. Indexed on
(author_id, created_at)from the Indexes chapter, sharded by user from the Sharding chapter. - Fan-out Queue — the write returns the instant the tweet is saved; the slow copying runs later, off the queue, so the user never waits for it.
- Social Graph — a separate store for “who follows whom.” It's a different access pattern than “a user's tweets,” so it gets its own home.
- Feed Cache — the precomputed per-user timelines. Just the Redis from the Caching chapter; workers push each new tweet in here.
- Feed read — one O(1) lookup. The feed service hands back the ready-made timeline; the per-user merge never touches the database.
- Dropped: read replicas for timeline assembly — the Tweet Store still keeps replicas for body hydration and celebrity pulls, but the timeline itself comes from cache.
Notice the shape: reads and writes have split into two independent paths that meet only at the Feed Cache — the write path fills it asynchronously, the read path drains it instantly.
Paging the feed as the user scrolls
A feed is effectively infinite, so we never hand back the whole thing — we send it a page at a time: the top twenty tweets, then the next twenty as the user scrolls. The only open question is how the server knows where to continue from.
In a normal app you'd reach for offset / limit — page three is LIMIT 20 OFFSET 40 (“skip 40, then give me 20”). It's the standard way to paginate, and for most APIs — a settings list, an admin table, ordinary search — it's completely fine. On a live, infinite feed it quietly falls apart, for two reasons:
- It gets slower the deeper you scroll.
OFFSET 40doesn't skip 40 rows — the database reads them and throws them away. Page 10 reads 200 rows to return 20; page 1,000 reads ~20,000 to return the same 20. Your most engaged users scroll deepest, so they get the slowest feed. - New tweets make it wrong. OFFSET counts positions, but on a live feed positions keep shifting. If three tweets land at the top while you're reading page 1, page 2 (
OFFSET 20) now points at rows you already saw — duplicates. Deletes shift the other way and silently skip tweets.
The fix is to page by landmark instead of by position — a cursor. The client remembers the ID of the last tweet it saw and asks for “twenty tweets older than that”: WHERE id < :last_seen_id ORDER BY id DESC LIMIT 20. Because tweet IDs are time-sortable (Snowflake-style, see Ch23) and indexed, the database jumps straight to that ID and reads exactly twenty rows — page one and page ten-thousand cost the same. And since you continue from a specific tweet rather than a row count, tweets arriving at the top can't shift your place, so the duplicates go away too.
One thing that trips people up: the feed you scroll isn't one list — it's two sources merged, the prebuilt cache plus a live celebrity pull. The merged result is never stored anywhere; it's rebuilt on every request. So what makes it paginate? The same cursor is simply re-applied to both sources — it works because both are sorted by tweet ID. Here it is page by page:
Two finishing touches: where media lives, and splitting the data
The core design is done. Two practical loose ends remain — and you've met both tools already, so this is about where to apply them, not learning anything new.
1. Images and video don't belong in the database. A tweet row is tiny text; a video is megabytes. Putting that media in the database would bloat every row and slow every read. Instead, store the file in blob storage (a service built for large files, like Amazon S3) and keep only a short link to it in the tweet row. Put a CDN in front so the file is cached close to each user and downloads fast. The database stays small and quick; the heavy bytes are served elsewhere.
2. One machine can't hold everyone's data. Past a certain size, no single machine has the storage or the throughput — this applies to both the Tweet Store and the Feed Cache. So we shard each of them — split the data across many machines — and the key choice is how we split it. We split by user ID, so all of one person's data (their tweets in the store, their precomputed feed in the cache) lands on the same shard. The common request is still served from the cache, exactly as before — sharding just means that lookup goes to the one cache shard holding this user's feed, instead of every machine having to be consulted. The fast O(1) read stays fast as the data grows.
-- ✅ HOT PATH — read my prebuilt feed (key = user_id → one cache shard) ZREVRANGE feed:u_bob 0 19 -- Redis · single-shard · O(1) -- ✅ one author's own tweets (key = author_id → one store shard) SELECT id, text FROM tweets WHERE author_id = 'u_alice' ORDER BY created_at DESC LIMIT 20; -- single-shard · uses the (author_id, created_at) index -- ⚠ CROSS-SHARD — merge many authors live (the celebrity pull, or analytics) SELECT id, text FROM tweets WHERE author_id IN (:followee_ids) -- hundreds of ids → touches many shards ORDER BY created_at DESC LIMIT 20; -- scatter-gather + merge · the expensive path
Step back and see what just happened. We didn't invent new machinery — we used caching, queues, workers, sharding, and a CDN, all from Part 1. What this archetype taught us was the thinking: spot the read-heavy ratio, refuse to do expensive work on the common path, move it to write time, then patch the one case (celebrities) where that trade backfires. That reasoning transfers directly to Instagram, Reddit, and any other feed.
Tradeoffs at each layer
Pulling it together — every layer made one choice to serve the 100:1 read ratio, and each carries a cost worth naming out loud:
Wrapping up, the way you'd say it out loud
“Twitter is a read-heavy feed, roughly a hundred reads per write, so I optimized the read path. The naive design that builds each timeline on read is too slow on the common path, so I precompute every user's feed at write time with asynchronous fan-out workers, making reads a single O(1) lookup. Pure fan-out on write breaks for celebrities, so I use a hybrid: push for normal users, pull-merge for the few celebrity accounts. Feeds are cursor-paginated for correctness on a live timeline, media sits behind a CDN, and everything is sharded by user ID. With more time I'd add feed ranking and a search and trends pipeline.”
The whole interview pivots on one move: don't present fan-out on write as a fact you memorized — derive it. Start from the read-heavy ratio, show why building feeds on read fails, and let that motivate precomputing at write time. Then proactively raise the celebrity problem and resolve it with the hybrid. A candidate who narrates that reasoning, instead of reciting the final diagram, reads as someone who can design the next problem too — which is exactly what the interviewer is trying to find out.