A feed, and the fan-out decision underneath it
Read-time or write-time is the only question here, and the honest answer is that a real system does both and switches between them per account.
The brief
Users follow other users. A user's feed shows recent posts from everyone they follow, newest first.
Some accounts have a handful of followers. A few have millions.
Requirements
Functional
- Post something
- Read your feed, paginated
- Follow and unfollow
Non-functional
- Opening the app is a feed read, so feed reads vastly outnumber everything else
- A post appearing a few seconds late is acceptable; a feed that fails to load is not
- The follower distribution is not a bell curve, and any design that assumes it is will fall over on the tail
Back-of-envelope
Assume
- 10 million daily active users
- Each opens the feed 10 times a day, and posts twice
- The average account has 200 followers, but the largest have 10 million
Therefore
- Feed reads: 1e7 × 10 / 86,400 ≈ 1,160 per second.
- Posts: 1e7 × 2 / 86,400 ≈ 230 per second.
- Fan-out on write, average case: 230 × 200 ≈ 46,000 feed inserts per second. Large, steady, and entirely manageable with a queue.
- Fan-out on write, worst case: ONE post by a 10-million-follower account is 10 million inserts. At 46,000/s of steady capacity that single post is roughly three and a half minutes of the entire system's write budget.
The average told you the system is fine. The tail told you the design. This is why the follower distribution is an assumption in its own right rather than a single average — writing down only the mean would have hidden the only interesting number here.
The interface
What is stored
id (time-sortable) · authorId · body · createdAtA time-sortable id means merging feeds is merging already-sorted lists. With a random id you would carry a timestamp alongside every entry and sort at read time, on the hottest path in the system.
userId → capped list of post ids, newest firstThe read is a range scan from the head. Capped because nobody scrolls to post 5,000, and storing it is paying rent on something no one reads.
followerId → followees, and followeeId → followersPush needs followers-of-an-author; pull needs followees-of-a-reader. The hybrid uses both, so both indexes exist, and that duplication is a real storage cost rather than an oversight.
The design
The decisions
Each of these could go the other way. The choice, the reason, and what it costs — a design that lists only what it chose teaches the choice; one that lists what it gave up teaches the judgement.
What breaks first
In order. Each names what you would actually observe, and each fix carries its cost.
When something fails
Scaling it
Each step is triggered by a number, not a feeling — and carries what it costs.
What gets probed
The design is the easy half. These are where the conversation goes, and each has a defensible answer above.
- An account with 10 million followers posts. Walk through the next five minutes.
- A user follows someone new. Does their back catalogue appear in the feed, and what did that cost?
- Where is the threshold between push and pull, and how would you know it is wrong?
- A post is deleted after fan-out. What does a follower's feed show?