Courses / System Design Interview / Chapter 28
Module 3 · Design Real Products · Chapter 28 of 39
Design a Microblog
Builds on What System Design Is: From One Server to Millions, Back-of-the-Envelope Estimates, Databases: Replication, Partitioning and Choosing a Store, Content Delivery Networks, Unique IDs and Sequencers, Distributed Caches, Message Queues, Publish-Subscribe, Blob Stores, Distributed Search and Sharded Counters. Next up: Design a News Feed.
Part 1 · Requirements and Scale
The brief
9:52 pm: India hit the last ball of a World Cup final for six. Kabir, a former fast bowler turned commentator with 30 million followers, posts two words: "WHAT A FINISH." Two million fans post in the same minute. On a first-draft design, Kabir’s post alone means 30 million timeline writes, queued ahead of everyone else’s. Ravi, who follows Kabir and 299 other accounts, refreshes and sees nothing new. He refreshes again, and each refresh asks the database to rebuild his timeline from scratch. The trophy is lifted before his timeline catches up. This chapter builds the design that keeps up.
The product: a microblog where people post short public messages, follow other accounts and read a home timeline of the newest posts from everyone they follow. Plan for about 200 million daily users. A few accounts, like Kabir’s, have tens of millions of followers. You are designing the posting path, the follow graph and the home timeline.
What we are building
Before any numbers, pin down what the product does, because the hard part only shows once the scope is clear.
A microblog is a service for short public posts. People publish a few lines of text, follow other accounts and read a home timeline: the recent posts of everyone they follow, newest first. Following is one-way, so Ravi can follow Kabir without Kabir following him back. That is how one account collects 30 M followers while most have a few dozen.
Writing a post is easy: one row per post. Reading is the hard part. Every time someone opens the app, the system must show a fresh timeline stitched from the posts of hundreds of accounts, in a fraction of a second, for hundreds of millions of people. The whole design follows from making that read cheap.
The scope below keeps the timeline at the centre and leaves out features that would each bring their own write path or read path.
| In scope | Out of scope |
|---|---|
| Post up to 280 characters, with an optional image | Direct messages |
| Follow and unfollow any account | Ads and promoted posts |
| Home timeline: posts from accounts you follow, newest first | A ranked feed (Design a News Feed covers ranking) |
| Profile timeline: one account’s own posts | Editing a post after publishing |
| Like and repost, with counts | Moderation pipelines in detail |
| Search posts by keyword; see trending topics | Notifications |
Everything on the right can be added later without changing how the home timeline is built, which is why it can wait. The next question is how fast, how fresh and how safe the timeline must be.
How well it must work
Scope says what the product does; non-functional requirements say how well, and they decide the architecture. Each one below gets a number or a clear rule.
- Fast reads. The home timeline returns in under 200 ms at the 99th percentile (p99), measured at the server: 99 of every 100 requests are faster than that.
- Read-heavy. People scroll far more than they post: about 20 timeline loads for every post written, and each load shows 20 posts.
- Available rather than strictly consistent. If Kabir’s post reaches a follower 5 seconds late, nobody is harmed. If timelines stop loading during the final, everybody notices. So timelines are eventually consistent: every copy catches up after a short delay.
- Durable. Once the app says "posted", the post is never lost.
- Read your own writes. Ravi always sees his own new post at once, even if his followers see it a few seconds later.
These assumptions feed every estimate in the chapter.
| Assumption | Value |
|---|---|
| Daily active users (DAU) | 200 M |
| Monthly active users | 300 M |
| New posts per day | 100 M (half a post per active user) |
| Timeline loads per active user per day | 10 |
| Average followers per account | 200 |
| Accounts an active user follows | About 300 (active users follow more than average) |
| Largest accounts | 30 M followers or more |
Two rows matter most: 200 followers on average, and 30 M for the largest accounts.
Follower counts are very uneven: most accounts have a few dozen, while a few thousand have millions. A design sized for the average of 200 can still collapse on that long tail, so every choice in this chapter is checked against Kabir as well as Ravi.
Estimates: the fan-out multiplier
With the assumptions set, the question is which number decides the design. Start with the cheap one. 100 M posts a day is about 1,160 posts a second (100 M ÷ 86,400), the arithmetic from Back-of-the-Envelope Estimates. One well-tuned database could absorb that alone.
The expensive number hides one step further. If every post id is copied into each follower’s timeline, one post becomes 200 writes.
Fan-out workers at the peak: ≈ 60 (about 12 at the daily average; a queue absorbs bursts above 60)
Read it as steps
Watch for the multiply step: 1,160 cheap posts a second become over a million timeline inserts at the peak of the final.
The follower count, not the post rate, sets the size of the system. Fan-out turns 1,160 posts a second into more than a million timeline inserts at the peak, and it says those inserts must run in the background through a queue, because nobody should wait for 200 writes.
The other estimates are quick, and each answers a different question.
| Quantity | Working | Result |
|---|---|---|
| Timeline loads | 200 M users × 10 ÷ 86,400 s | ≈ 23,000 /s; ≈ 115,000 /s at peak |
| Post text | 100 M posts × 1 KB a day | ≈ 100 GB/day; ≈ 37 TB/year, ≈ 110 TB with 3 copies |
| Images | 10 M images × 200 KB a day | ≈ 2 TB/day, kept in a blob store |
| Timeline cache | 300 M monthly actives × 800 ids × 8 bytes | ≈ 1.9 TB of raw ids; about 4 TB stored |
Storage is ordinary; the timeline inserts and the timeline cache are what need design. The 1 KB per post is generous on purpose: 280 Devanagari characters take up to 840 bytes in UTF-8, at 3 bytes each, and the post also carries ids, a timestamp and flags.
Part 2 · API, Data and the First Design
The API
The estimates say reads dominate, so the API should keep both paths simple. A handful of calls covers every requirement, and the server learns who is calling from the auth token, never from a user id in the body.
POST /v1/posts {"text": "What a finish", "media_ids": ["m_91"]}
Idempotency-Key: 6f1c2a -> 201 {"post_id": "1834092", "created_at": "..."}
DELETE /v1/posts/{post_id} -> 204
PUT /v1/follows/{user_id} -> 204 (follow; repeating it is harmless)
DELETE /v1/follows/{user_id} -> 204 (unfollow)
GET /v1/timeline/home?limit=20&cursor=1834092 -> 200 {"posts": [...], "next_cursor": "1829117"}
GET /v1/users/{user_id}/posts?limit=20&cursor=...
PUT /v1/posts/{post_id}/likes/me -> 204
GET /v1/search?q=last+over&cursor=...Images are uploaded to the blob store first; the post refers to them by media id.
Idempotency key. Phones on crowded stadium networks time out and retry. The app attaches a random key to each new post; if the server has seen that key in the last 24 hours, it returns the original result instead of creating a second, identical post. Without it, every timeout during the final becomes a duplicate.
Cursors, not page numbers. New posts arrive at the top while Ravi scrolls, so "page 3" points somewhere different every second and he sees the same posts again. A cursor that says "posts older than id 1829117" is not moved by new posts at the top, so the next page carries on exactly where the last one ended. This works because the timeline is sorted by id; Design a News Feed shows what changes when the list is ranked instead.
Following the same account twice should still leave one follow. HTTP defines PUT as idempotent: sending it again leaves the same state, so a retry after a timeout does no harm.
Each call reads a different shape of data. The next page decides where each shape lives.
Data model
Four kinds of data sit behind those calls, and each is read in its own way. Pick storage from the access pattern, as the Databases chapter recommends, not from habit.
user_id (pk)handle (unique)namebiofollower_countcreated_atpost_id (pk, time-ordered)author_idtextmedia_idsreply_tocreated_atdeletedfollower_id (pk part 1)followee_id (pk part 2)created_atpost_id (pk part 1)user_id (pk part 2)created_at- posts author_id → users
- follows follower_id → users
- follows followee_id → users
- likes post_id → posts
- likes user_id → users
The table maps each question the system asks to the layout that answers it from one partition.
| Question the system asks | Shape of the read | Where it lives |
|---|---|---|
| Fetch these 20 posts | Point lookups by id | Post store (sharded relational), hash-partitioned by post_id, with a post cache in front |
| One author’s posts, newest first | Range scan by author | A user_posts index, partitioned by author_id and sorted by post_id |
| Who follows this author? | Scan of up to millions of rows | A followers table, partitioned by followee_id |
| Whom does this user follow? | Short list, read often | A following table, partitioned by follower_id |
| This user’s home timeline | Newest 800 ids, read constantly | Timeline cache, partitioned by user_id |
The follow graph is stored twice, once by follower and once by followee, because fan-out asks "who follows Ravi?" while timeline rebuilds ask "whom does Ravi follow?". Two writes per follow is a small price for answering each from a single partition.
Use time-ordered 64-bit post ids, as built in Unique IDs and Sequencers: sorting by id is then sorting by time, and the id doubles as a cursor. Do not range-partition the post store on that id, though. Every new post would land in the newest range, so one shard would take all 1,160 writes a second while the rest sat idle. Hash the id to pick the shard, and keep time order inside each author’s user_posts list.
High-level design
With the data placed, the system splits into two paths. The write path saves a post and hands a job to a queue. The read path serves a home timeline from a cache. Fan-out workers join the two: they read the author’s follower list and push the new post id into each follower’s cached timeline. Follow one of Ravi’s posts through it.
- 1The post service saves the post and an outbox row in one transactiona crash between the two cannot lose the fan-out job
- 2It adds the id to Ravi’s own timeline and replieshe sees his post at once, before any fan-out
- 3A relay moves the outbox row to the fan-out queue and the post streamsearch and trending read the stream
- 4A fan-out worker reads Ravi’s 200 followersfrom the followers table
- 5The worker inserts the id into each follower’s timeline listdone within a second or two
- 6A follower opens the app; the timeline service reads her list and hydrates 20 poststwo cache reads, about 16 ms on the server
Each component has one job:
- Post service: assigns a time-ordered id and writes the post plus the outbox row on its shard, the outbox pattern from Message Queues.
- Fan-out queue and workers: a durable queue, so a burst of posts becomes a backlog instead of an outage.
- Timeline cache: one capped list of post ids per user, in memory, sharded by user id.
- Timeline service: reads that list, then hydrates it: turns ids into full posts with author names, counts and image links, read from a post cache.
- Post stream: a topic, as in Publish-Subscribe, that search, trending and notifications consume at their own pace.
The expensive work happens at posting time, where nobody waits for it. That choice is called fan-out on write. Is it always the right one?
Part 3 · Deep Dives
Fan-out on write or on read
This is the decision the rest of the design hangs on: where the work of building a timeline happens, and who waits while it runs.
Think of a newspaper. Home delivery does the work before anyone wakes up, once per subscriber. A newsstand does nothing until a reader walks over and collects each paper. Timelines can be built either way.
Fan-out on write, also called push, copies each new post id into every follower’s timeline when the post is written. Fan-out on read, or pull, does nothing at posting time: when Ravi opens the app, it fetches the recent posts of all 300 accounts he follows and merges them. Set side by side, the two fail in opposite places.
| Fan-out on write (push) | Fan-out on read (pull) | |
|---|---|---|
| Work per post | One insert per follower: 200 on average, 30 M for Kabir | One write |
| Work per timeline load | Read one ready-made list | Fetch recent posts from about 300 accounts and merge |
| Read latency | Low and steady | Grows with the number of accounts followed |
| Freshness | A few seconds behind while fan-out runs | Always current |
| Wasted work | Lists built for people who never open the app | None |
| Breaks on | Accounts with millions of followers | Read traffic: 23,000 loads a second × 300 ≈ 7 M lookups a second |
Push breaks on huge accounts; pull breaks on read traffic. The daily totals settle the default. Push costs 100 M posts × 200 followers = 20 B small inserts, all in the background. Pull costs 2 B loads × 300 accounts = 600 B lookups, each inside a request a person is waiting for. Push does about 30 times less work, and none of it while anyone waits.
Kabir’s one post is 30 M inserts, as much as the whole system’s average fan-out for over two minutes (30 M ÷ 230,000 a second ≈ 130 s). Even if that one job got 100,000 inserts a second, his last follower would see it 5 minutes late, and every post queued behind it would wait too.
So push is the right default, but not for Kabir. The next page splits the two.
The hybrid: push for most, pull for the famous
Push works for Ravi and fails for Kabir, so treat the two kinds of account differently. Pick a threshold, say 100,000 followers. Posts from accounts below it are pushed as before, so no single post costs more than about 100,000 inserts. Posts from accounts above it, the high-fan-out accounts, are saved but never fanned out. They are pulled when a reader opens the app and merged into the pushed list.
Pulling sounds like the expensive path we just rejected, but here it is cheap. A typical reader follows only a handful of such accounts, and their recent posts are read by millions, so they almost always sit in the post cache. Pulling five of them costs a few milliseconds.
Predict before you watch: Kabir’s post is never pushed. When Ravi opens the app a minute later, does he see it, and where does it come from?
- Yes. The timeline service reads Kabir’s recent posts from the post cache and merges them with Ravi’s pushed list.
Ravi posts. It is saved, and one fan-out job joins the queue
Read it as steps
Watch for the queue badge when Kabir posts: pushed, his post buries everyone else’s; skipped and merged at read time, it costs Ravi one cache read.
Pushing Kabir’s post would have queued 30 M inserts ahead of every other post; skipping it costs each of his readers one extra cache read. The hybrid moves the work for a few huge accounts from posting time to reading time, where caching makes it nearly free because millions of readers share the same few posts.
Merging at read time
The merge is where the two halves meet, so it must be fast and stay correct when they overlap. Here is the whole read path in a few lines.
def home_timeline(user_id, limit=20, before=None): ids = timeline_cache.range(user_id, before=before, count=limit) # pushed ids for star in following_high_fanout(user_id): # usually 0 to 10 ids += user_posts.recent(star, before=before, count=limit) # hot in cache ids = sorted(set(ids), reverse=True)[:limit] # newest first return hydrate(ids) # full posts; drops deleted posts and blocked authors
Time-ordered ids make the merge a sort of a few hundred integers, and set() removes duplicates.
One load of Ravi’s timeline, with 295 accounts pushed and 5 pulled, breaks down like this.
| Step | Work | Time, roughly |
|---|---|---|
| Read his pushed list | 1 cache read | 2 ms |
| Read the 5 big accounts’ recent posts | 5 cache reads in parallel | 3 ms |
| Merge and keep the newest 20 | Sort a few hundred ids | under 1 ms |
| Hydrate 20 posts with authors and counts | One batched read of 20 keys | 10 ms |
| Total on the server | about 16 ms |
The load uses under a tenth of the 200 ms budget. Three races need care:
- Crossing the threshold. An account that goes viral can pass 100,000 followers in a day. Re-check the flag every few hours, not on every follow; a post both pushed and pulled during the switch still shows once, because the merge removes duplicate ids.
- The author’s own post. Fan-out is asynchronous, so the post service writes the id into the author’s list before replying, or Ravi thinks his post failed and posts again.
- Late jobs. A delayed job can deliver a 10:00:01 post after a 10:00:05 one. The list is kept sorted by id, so the late id lands in its place.
The threshold is a dial, not a constant: set it so a typical reader merges no more than about 10 pulled accounts.
Fan-out jobs: chunks and backlogs
Below the threshold, push stays the default, and the jobs themselves need shaping. One post can still mean up to 100,000 inserts. As a single job, that keeps one worker busy for about five seconds at 20,000 inserts a second. If the worker crashes near the end, the whole job runs again, and every post queued behind it waits.
So split big jobs into chunks. The first job for a post reads the author’s follower list a page at a time and queues one chunk job per 1,000 followers, each naming the post id and its slice of the list. A 90,000-follower post becomes 90 chunks that many workers run at once, finishing in well under a second. A crash repeats one chunk, and that is harmless: inserting an id that a sorted list already holds changes nothing. Ravi’s 200-follower post stays a single job.
Kabir’s post, pushed, would still be 30,000 chunks and 30 M inserts. That is why the threshold exists. Accounts just below it, with tens of thousands of followers, also get their own queue, so their chunks cannot delay ordinary posts.
Two habits cut the work further:
- Skip inactive users. Do not push to accounts that have not opened the app in 30 days. When one returns, build the timeline once with fan-out on read, then put them back on the push list.
- Push to the recently active first. During a backlog, order chunks so people who opened the app in the last hour get the post first; the rest can wait a minute.
Size the tier from the estimate scene: at about 20,000 batched inserts a second per worker, the 230,000 per second average needs about 12 workers and the final’s 1.2 M peak about 60. Scale between the two with the queue depth, and let the queue absorb anything above 60 as a few seconds of lag rather than errors. Every insert lands in the timeline cache, which the next page sizes.
The timeline cache: sizing
Every insert from the workers lands in the timeline cache, so it must hold every active user’s list and take a million writes a second at the peak. How big is it?
Each user’s timeline is a list of post ids, newest first, capped at 800 entries. A push is two cheap steps: insert the id, then trim back to 800. If someone scrolls past the end, the timeline service falls back to fan-out on read for older pages.
The raw ids are small: 300 M monthly actives × 800 × 8 bytes ≈ 1.9 TB. How they are stored decides the real size. A compact packed encoding roughly doubles it with keys and slack, to about 4 TB. A general-purpose sorted structure with pointers per entry can cost 5 to 10 times the raw bytes, which would mean 10 to 20 TB, so the encoding is a cost decision, not a detail. The choices below assume the compact one.
| Choice | Value | Why |
|---|---|---|
| What is stored | Post ids only, 8 bytes each | Posts change (likes, deletes); ids never do |
| Entries per user | 800 | About 40 screens of 20 posts |
| Who gets a list | Users active in the last 30 days | No memory spent on people who left |
| Sharding | By user id, with consistent hashing | Even spread; adding a node moves few users |
| Copies | Two copies of each shard | A node failure does not trigger mass rebuilds |
| Servers | 4 TB ÷ 64 GB ≈ 60, about 120 with the second copy | Memory sets the count, not traffic |
Memory, not traffic, sets the server count. At the peak, each of 60 primaries takes about 20,000 inserts and 2,000 timeline reads a second (1.2 M and 115,000 spread across them), far below the roughly 100,000 operations a second one cache node handles.
Hydration, deletes and misses
The cache holds only ids, so every load turns ids back into posts. That step, hydration, is also where deletes and blocks take effect.
Hydration sends one batched multi-get, a single request for many keys, to the post cache, which holds post objects by id, then does the same for author profiles. A popular post appears in millions of timelines, and Kabir’s is read by millions of people, but each is cached once, so the post cache’s hit ratio is very high. Hot posts also sit in each server’s local memory for a few seconds, so millions of readers do not all hit one cache key.
Storing ids instead of whole posts is what makes changes cheap. If timelines held copies of posts, deleting a post from a 90,000-follower account would mean rewriting 90,000 lists, and every like would change them all again. With ids, a delete marks the post deleted in the post store and the post cache, and hydration drops any id whose post is deleted or whose author you have blocked or muted. The stale id falls off the end of each list as newer posts push it past 800.
An unfollow uses the same filter until the old ids age out. A new follow copies that account’s recent posts into your list in the background, so the next load already includes them.
If a user’s list is missing (a new user, an evicted entry or a lost cache node), the timeline service rebuilds it with fan-out on read and writes it back. That first load takes about 100 ms instead of 16; later loads are fast again. Rebuilds are rate-limited, so a lost node cannot send a wave of them to the database.
With every part in place, the next question is what breaks first as load grows, and what users notice when it does.
Part 4 · Failures and Trade-offs
Bottlenecks and failures
Each part of the design fails in its own way. The table lists each failure, what users see, and how the design copes.
| What goes wrong | What users notice | How the design copes |
|---|---|---|
| Fan-out queue backs up during the final | Posts reach followers late | Add workers; push to recently active users first; lower the pull threshold for the evening; timelines lag but keep loading |
| A timeline cache node dies | A slower first load for some users | Its replica takes over; missing lists are rebuilt by fan-out on read, rate-limited to protect the database |
| One post is read by millions | Nothing, if handled | The post sits on several cache replicas and in each server’s local memory for a few seconds; images come from the CDN |
| Kabir’s follower list is huge | Slow follower pages for that account | He is on the pull side anyway; split follower lists into buckets so no partition holds 30 M rows |
| The search indexer falls behind | New posts missing from search for a while | It is a separate consumer of the post stream; it catches up without slowing posting |
| A retried request posts twice | A duplicate post | Idempotency key on POST /v1/posts |
| A zone or a whole region fails | A zone: almost nothing. A region: slower first loads, and a few seconds of posts arrive late | Every shard keeps a replica in another zone. If a region fails, its users move to another region, which rebuilds timelines on demand at a capped rate; posts not yet copied across appear when the region returns |
In almost every row the timeline keeps loading: failures show up as lateness or a slower first load, rarely as errors. That is the eventual consistency chosen in the requirements, paying off.
Trade-offs, monitoring and cost
Every choice in this design bought something and paid for it. Push spends memory and background work to make reads cheap, and accepts a few seconds of staleness. The hybrid adds a second way to build a timeline, more code and more tests, in exchange for never melting on a post like Kabir’s.
Running in two regions adds one more trade. Each region keeps its own timeline caches and fan-out workers, and posts are copied between regions asynchronously, so a post written in Mumbai reaches a follower served from another region a second or two later. Copying synchronously would make every post wait for a cross-region round trip, about 200 ms to another continent, to buy freshness nobody asked for.
Three signals show whether the design is healthy:
Image egress from the CDN dominates the bill. One post in ten carries an image, so 23,000 loads a second × 20 posts pull about 46,000 images a second: at 100 KB for a feed-size copy, roughly 12 PB a month, about $600,000 at $0.05 per GB (check current prices). Memory for about 120 cache servers comes next. Post text, at 37 TB a year, is cheap.
Part 5 · Going further
Search, trending and counts
Optional: three features that grow from the post stream once timelines work. None of them sits on the posting path, so if one falls behind, posting and timelines carry on unaffected.
Search. An indexer reads new posts from the stream, splits the text into terms and adds the post id to each term’s list in an inverted index, as in Distributed Search. People mostly search for what is happening now, so the index is split by time: a small, hot index for the last few days and larger, colder ones behind it. A query reads the hot index first and goes further back only when it needs more results.
Trending. Count hashtags and phrases in short windows, say 5 minutes, per region. A topic trends when its count is far above its usual count for that hour, not merely when it is large. A greeting hashtag that is big every morning is not news; a player’s name jumping from 200 to 40,000 mentions in the last over is. The pipeline has five steps:
- 1Read posts from the streamskip known spam accounts and copy-pasted text
- 2Pull out candidate termshashtags, names, short phrases
- 3Count per 5-minute window and regionapproximate counters such as a count-min sketch, which trades a little accuracy for fixed memory
- 4Compare with the usual count for that hourthe ratio, not the raw count, decides
- 5Publish the top 30 per region every minuteapps read a small cached list
Likes and reposts. Kabir’s post can collect thousands of likes a second, all aimed at one number. Use Sharded Counters: each like increments one of N shard counters, and readers see a cached sum refreshed every few seconds. Store the like itself as a row keyed by post and user, and add to the count only when that row is new, so liking twice changes nothing. A repost is a small post that points to the original, so it fans out like any other post and hydration shows the original inside it.
Part 6 · Interview kit
The 45-minute answer
Here is the whole answer on the interview clock: about 40 minutes of design once introductions are done.
| Minutes | What you say and draw | Key number or decision |
|---|---|---|
| 0–5 | Requirements: post, follow, home timeline newest first, likes. Out: DMs, ads, ranking, edits. Timelines may lag seconds; authors see their own posts at once. | Reads beat writes about 20 to 1; p99 under 200 ms |
| 5–8 | Only the numbers that decide something: posts a second, then the fan-out multiplier. | 1,160 posts/s × 200 followers ≈ 230,000 inserts/s; 1.2 M at peak |
| 8–13 | POST /posts with an idempotency key; GET /timeline with a cursor. Posts, follows stored both ways, timeline lists of ids. | Time-ordered ids, hash-partitioned |
| 13–22 | Post service with an outbox, fan-out queue and workers, follower graph, timeline cache, timeline service, post cache. Walk one post and one load. | Fan-out on write; a load is one cache read plus hydration |
| 22–37 | The 30 M-follower account: threshold, merge at read time, chunked jobs. Then the cache: ids only, 800 per user, deletes at hydration. | Pull above 100,000 followers; chunks of 1,000; about 120 cache nodes |
| 37–40 | A cache node and a region failing, what you monitor, what you would add next. | Alert on fan-out lag |
Pick the deep dive from the interviewer’s questions. Famous accounts, write load or late posts: take the hybrid and chunked jobs. Latency, memory or deletes: take the timeline cache and hydration. Search or trending: spend five minutes on the post stream consumers, then come back.
- Say
- "Reads outnumber writes about 20 to 1, so I precompute timelines with fan-out on write and accept a few seconds of lag."
- Say
- "Accounts above 100,000 followers are pulled at read time and merged, so one post never becomes 30 M writes."
- Say
- "Timelines hold 8-byte ids, not posts, so a delete changes one row, not millions of lists."
- Avoid
- "Each timeline is a join of follows and posts." At 23,000 loads a second, each merging about 300 accounts, the database melts.
- Avoid
- "Every post fans out the same way." One 30 M-follower post becomes 30 M inserts and delays everyone else’s posts by minutes.
Interview drill
Interviewers often change the problem halfway to see whether the design bends or breaks. Answer each one before opening it.
The final ends and posting jumps to 10× normal, about 11,600 posts a second, for 20 minutes. What breaks first, and what do you change?
- Fan-out: about 2.3 M inserts a second against workers sized for 1.2 M. The queue holds the excess, so posts arrive late, not lost.
- Add workers: 120 cache nodes take about 12 M operations a second, so workers, not the cache, are the limit.
- Push to recently active users first, and lower the pull threshold to 10,000 followers for the evening.
Product adds Lists: a user groups up to 5,000 accounts and reads them as a separate timeline. What changes?
- New tables lists and list_members, stored both ways: list to members, and member to the lists holding them.
- A list is another timeline, so push or pull returns. Most lists are read rarely: build them on read and cache the result for a minute.
- Push only into lists read daily; big accounts stay on the pull side, and the merge removes duplicates.
Finance wants the timeline cache bill halved, with p99 still under 200 ms. What do you change?
- Keep 400 ids per user, not 800: 20 screens still come from memory; deeper scrolls fall back to pull.
- Cache only users active this week; a returning user pays one rebuild of about 100 ms.
- Keep the second copy, or a lost node sends a wave of rebuilds to the database. Watch the miss rate after each change.
- Common miss
- Treating the 100,000 threshold as fixed. It is a dial: lower it during a spike, and set it from how many big accounts a typical reader follows.
- Common miss
- Forgetting the author’s own post. Fan-out is asynchronous, so write the id into the author’s list first, or they think the post failed and post it again.
Wrap-up
Use this page to revise before a mock round.
Next, Design a News Feed keeps this fan-out machinery but replaces newest-first with ranking: candidate pools, scoring signals and cursors into a frozen list.
Part 7 · Check yourself
Quick check
Q1A teammate’s prototype builds each home timeline with a join of follows and posts. At 20,000 loads a second, p99 passes 2 seconds and database CPU sits at 100%. What is wrong, and what is the fix?
Think it through, then check.
- Each load reads recent posts from about 300 accounts and sorts them: about 6 M lookups a second, repeated even when nothing new was posted.
- Fix: precompute with fan-out on write into a timeline cache, pull only big accounts at read time, and hydrate from a post cache.
Q2Users may now edit a post within 30 minutes of publishing. What changes?
Think it through, then check.
- No fan-out: timelines hold ids, so an edit updates one row on the post’s shard and refreshes its post cache entry.
- Keep earlier versions in a post_versions table and show an "Edited" label, so replies still make sense.
- Check the 30 minutes on the server from the time inside the post id, and publish a "post edited" event so search re-indexes it.
Q3Your pull threshold is 100,000 followers. What do you gain and lose by lowering it to 10,000, or raising it to 1 M?
Think it through, then check.
- Lower: smaller fan-out jobs and less lag, but each load merges more pulled accounts, so read latency rises.
- Raise: reads stay simple, but one post can cost 1 M inserts, about 1,000 chunks, and lag grows during the final.
Q4A fan-out worker crashes after pushing a post id into 600 of the 1,000 timelines in its chunk. What happens next, and why is it safe?
Think it through, then check.
- The chunk was never acknowledged, so the queue gives it to another worker after its timeout.
- That worker pushes all 1,000 again; the 600 lists that already hold the id do not change, because the insert is idempotent.
- Only 1,000 inserts repeat, not the whole post’s fan-out: that is what chunking buys.
Q5Which building block sits between saving Ravi’s post and updating his followers’ timelines, and why not call the fan-out workers directly?
Think it through, then check.
- A durable message queue, fed by an outbox (Message Queues), so a crash after the save cannot lose the job.
- Posting returns after one write instead of waiting for 200, or 100,000, inserts.
- A burst during the final becomes a backlog that workers drain, not an outage; at-least-once delivery is safe because inserts are idempotent.