Design a newsfeed (Twitter/Instagram-style)
📖 Walk me through it — plain English
This lesson is about building the "home feed" you see on Twitter or Instagram — the scrolling list of posts from people you follow. A feed (also called a timeline) is simply a personalized, time-ordered list of posts: yours is different from mine because we follow different accounts. The whole interview hinges on one question: when do you do the work of assembling someone's feed — at the moment a post is created, or at the moment someone opens the app? The hard part is that one person might follow thousands of accounts, and one popular account might have millions of followers — so naive approaches fall over at scale.
There are two basic strategies, and the jargon for them is "fan-out." Fan-out just means "spread one post out to many places." Fan-out on write (push) means: the moment Alice posts, you immediately copy that post into a pre-built feed list for every single follower. So when a follower opens the app, their feed is already sitting there ready — reading is instant. The cost is that posting is expensive: one post by someone with a million followers triggers a million copy operations. Fan-out on read (pull) is the opposite: you store nothing in advance. When a follower opens the app, you go fetch the latest posts from everyone they follow and merge them on the spot. Posting is cheap (you just save the post once), but reading is expensive — especially if they follow thousands of people, because you have to gather and sort from all of them every time.
Think of it like a neighborhood newsletter. Push is the publisher printing a personalized copy and stuffing it into every subscriber's mailbox the instant a new article is written — subscribers just walk to their mailbox (instant read), but if there are a million subscribers that's a million deliveries per article (slow write). Pull is the subscriber, each morning, walking to every author's house and asking "anything new?" then sorting it themselves — the authors do nothing extra (cheap write), but the subscriber's morning errand gets brutal if they follow a thousand authors (slow read).
Neither extreme wins, so the real-world answer is the hybrid, and that's the punchline to give in the interview. For normal accounts you push (pre-build follower feeds, so reads stay fast). For "celebrities" — the handful of accounts with enormous follower counts, sometimes called the hot-key or celebrity problem because they concentrate a huge amount of work on one key (one account) — you don't push, because the write storm would be crippling; instead you pull their recent posts at read time and merge them into the already-built feed. You get cheap reads for the common case and avoid the celebrity write explosion. After merging, a ranking step (recency, how close you are to the author, engagement, post type) reorders the candidate posts before returning them.
How to approach this kind of system-design question:
- State the core tension out loud first: push = cheap read / expensive write; pull = cheap write / expensive read.
- Name the breaking case for each (celebrities break push; following thousands breaks pull), then land on the hybrid.
- Pick concrete storage: posts in a key-value store (a simple "look up by id" database) keyed by post id; each user's timeline as a Redis sorted set (a list auto-ordered by a numeric score — here the timestamp) capped at ~1000 entries to bound memory; media in blob storage (S3) behind a CDN (edge servers that cache files near users).
- Close with scale concerns: viral posts (cache hard), celebrity write storms (absorb with a queue plus backpressure — slowing intake so the queue doesn't overflow), and cross-region (per-region caches; it's fine if follow changes take a moment to propagate everywhere — "eventual consistency").
Why the hybrid works: it spends effort where it's cheap and avoids it where it's catastrophic. The vast majority of accounts have modest follower counts, so pushing their posts is fine and keeps reads instant for everyone. Only the rare celebrity would cause a write explosion, and those are few enough that pulling their posts on demand is affordable. You optimize the common path (fast reads) without paying the worst-case write cost.
A quick on-ramp: the words you'll need
Before the design, here are the load-bearing terms — each defined once, in plain English, so the rest of the page stands on its own.
- Feed / timeline — a personalized, time-ordered list of posts from the accounts you follow. "Build a feed" = produce that list for one user.
- Fan-out — the act of taking one new post and spreading it to many destinations (the followers' feeds). "On write" = do it when the post is created; "on read" = do it when a feed is requested.
- Push vs pull — synonyms for fan-out-on-write vs fan-out-on-read. Push pushes the post out to followers ahead of time; pull makes the reader pull posts in on demand.
- Cache — a fast, in-memory store (e.g. Redis) holding pre-computed answers so you don't recompute them on every request. Here, each user's pre-built timeline lives in a cache.
- Hot key / celebrity problem — when one item (one account, one post) gets so much traffic that it overwhelms the single machine or queue responsible for it. Celebrities cause hot writes (fan-out storms); viral posts cause hot reads.
- Denormalization — deliberately storing the same data in more than one place (e.g. copying a post into every follower's feed) to make reads faster, accepting extra writes and storage as the cost. Push is denormalization.
- Pagination / cursor — returning a feed in chunks (a "page" of, say, 20 posts) instead of all at once. A cursor is a bookmark ("give me posts older than this timestamp/id") so the next page picks up exactly where the last one stopped, even as new posts arrive at the top.
- Eventual consistency — a guarantee that all copies of the data will agree eventually, but may briefly disagree right after a change. A new follow taking a few seconds to show up everywhere is acceptable; nobody is harmed by a short delay.
▶ Core tension — fan-out on read vs write
- Fan-out on write (push): when A posts, copy into the timeline of every follower. Read is cheap, write is expensive. Bad for celebrities with millions of followers, because one post triggers millions of writes.
- Fan-out on read (pull): at read time, fetch latest posts from everyone you follow and merge. Write is cheap, read is expensive. Bad if you follow thousands, because every feed open re-queries and re-sorts all of them.
- Hybrid (best in practice): push for normal users, pull for celebrities, merge at read time. The default answer.
The key tradeoff — when to use which
The single number that decides it is the read/write ratio — how many times a piece of content is read versus written. Feeds are read-heavy (people scroll far more than they post), which is exactly why pre-building feeds on write (denormalization) usually wins: you pay the write cost once and serve millions of cheap reads. So:
- Use push when followers-per-author is small and reads vastly outnumber writes — the normal case. Reads become a single cache lookup.
- Use pull when followers-per-author is huge (celebrities) so push would explode, or for accounts so rarely read that pre-building is wasted work (inactive users). Fetch + merge on demand instead.
- Use the hybrid in practice: push for the long tail of normal accounts, pull for the short list of celebrities, and merge the two at read time. Pick a threshold (e.g. > ~100k followers ⇒ treat as celebrity) to draw the line.
A concrete data model
Designs feel abstract until you write down the actual records. Here are the four pieces of state the system keeps. (A KV store is a key-value database — you hand it a key, it hands back the value; think a giant hash map that survives restarts. A Redis sorted set keeps members ordered by a numeric score, here the timestamp, so reading "newest first" is trivial.)
# 1. Posts — source of truth, in a KV store keyed by post_id
# Sharded (split across machines) by post_id so load spreads evenly.
post:9f2c -> { author: "alice", text: "hi", ts: 1733000000,
media: "s3://.../p9f2c.jpg" }
# 2. Follow graph — who follows whom (its own service, sharded by user)
followers:alice -> { bob, carol, dave, ... } # used by push (fan-out targets)
following:bob -> { alice, ed, celeb_x, ... } # used by pull (whose posts to fetch)
# 3. Timeline cache — each user's PRE-BUILT feed, a Redis sorted set
# score = timestamp, so range reads return newest-first. Capped ~1000.
timeline:bob -> ZSET[ (post:9f2c, 1733000000), (post:7a1e, 1732999000), ... ]
# 4. Celebrity list — the few accounts we do NOT push (pull at read time)
celebrities -> { celeb_x, celeb_y, ... } # e.g. anyone > ~100k followers
▶ Architecture
client → LB → API → post service → write to KV (posts) + enqueue fan-out
↓
fan-out worker → for each follower: prepend to their timeline cache
(skip for celebs — they pull-merge at read)
client → API → timeline service → read user's pre-built timeline + merge celeb posts
→ rank → return
LB is a load balancer — it spreads incoming requests across many API servers so no single one is swamped. The queue between the post service and the fan-out worker decouples the two: posting returns the instant the post is saved and the fan-out job is enqueued, while the slow work of copying it into follower timelines happens asynchronously in the background. That keeps posting fast even when fan-out is heavy.
The write path, traced end to end
Walk through what happens when Alice (a normal account) makes a post:
- The client sends the post to the API (via the load balancer). The post service writes one record to the posts KV store (
post:9f2c) — this is the single source of truth. - The post service enqueues a fan-out job and immediately returns success to Alice. She doesn't wait for fan-out; her post is already durably saved.
- A fan-out worker picks up the job, asks the follow-graph service for
followers:alice, and for each follower prepends(post:9f2c, ts)to that follower'stimeline:<follower>sorted set in the cache. This is the denormalization step — one post, copied into many feeds. - If Alice were on the celebrity list, the worker would skip fan-out entirely — her followers will pull her recent posts at read time instead, avoiding the write storm.
The read path, traced end to end
Now Bob opens the app and requests his feed:
- The timeline service reads Bob's pre-built feed: a range query on
timeline:bobreturning the newest ~N post-ids. Because it's a sorted set in a cache, this is one fast lookup — no merging across thousands of authors. This is the payoff of pushing. - It checks which celebrities Bob follows (
following:bob ∩ celebrities) and pulls their recent posts directly from the posts store. These weren't pushed, so they must be fetched now and merged into the candidate list. - The merged candidate set (~500 posts) goes through ranking — reordered by recency, author affinity, engagement, and post type — then the post-ids are hydrated (the full post bodies and media URLs fetched from the KV store and CDN).
- Results are returned paginated: only the first page (~20 posts) plus a cursor (a bookmark like "older than ts=1733000000"). When Bob scrolls, the next request sends the cursor and gets the next page, so new posts arriving at the top don't shift or duplicate what he's already seen.
▶ Storage choices
- Posts: KV store keyed by post_id, sharded by post_id (random). Random sharding spreads load evenly so no single shard becomes a hot spot.
- Timelines: Redis sorted set per user, score = timestamp. Cap at ~1000 entries to bound memory (a feed nobody scrolls past 1000 posts doesn't need to store more). Sorted set gives fast inserts and fast newest-first range reads.
- Follows graph: separate service (sharded by user). Most-followed users surfaced as the "celebrity list" so the fan-out worker knows whom to skip.
- Media: S3/blob storage (object storage for large files), CDN-cached so images/video are served from edge servers near the user. Posts store only the URL, not the bytes.
▶ Ranking
- Pure chronological (strict newest-first) is simplest but ad/engagement teams hate it — it can't surface the most relevant or revenue-driving posts.
- Signal mix: recency · author affinity (how much you interact with that author) · post engagement (likes/replies so far) · type weight (photo > text).
- An ML ranking layer runs over a candidate set (~500 posts) from the merged timeline. You first gather candidates (push + celeb pull), then score and reorder them — gather, then rank.
▶ 10× scale concerns
- Hot post (viral): a single post read by millions — a hot read key. Cache aggressively; serve static media from the CDN. The fix is read caching, not more writes.
- Celebrity write storm: the fan-out queue can pile up when many high-follower accounts post at once. Absorb with backpressure (slow intake so the queue can't overflow) + retry. This is the main reason celebrities are pulled, not pushed, in the first place.
- Cross-region: per-region timeline caches so reads stay local and fast. Eventual consistency on follows is acceptable — a new follow taking a few seconds to appear in every region harms no one.
Scaling pitfalls to call out
Beyond the headline concerns above, these are the traps interviewers probe for — naming them unprompted signals depth:
- The threshold is a knob, not a constant. "Celebrity" isn't a fixed number — pick a follower threshold and be ready to tune it. Too low and you pull too much (slow reads); too high and a near-celebrity's push still storms.
- New follows and backfill. When Bob follows Alice, his pre-built timeline has none of her past posts. Either backfill the last few on follow, or accept the feed fills in going forward. Don't silently leave it broken.
- Inactive users waste pushes. Pushing to followers who never open the app burns writes and memory for nothing. A refinement: only maintain timelines for recently-active users; pull (rebuild) on demand for the rest.
- Deletes and edits. A pushed post is now copied across many timelines. Deleting/editing means fixing every copy — or filtering deleted post-ids at read time (cheaper, common choice). This is the cost of denormalization.
- Thundering herd on cache miss. If a popular user's timeline is evicted from cache, many simultaneous reads can all try to rebuild it at once and hammer the backend. Guard with a single-flight lock or request coalescing so only one rebuild runs.
Takeaway: a newsfeed is a read-heavy system, so you pre-build feeds on write (push) to make the common read a single cache lookup — that's denormalization, trading extra writes and storage for fast reads. Push breaks for celebrities (write storms), so the production answer is the hybrid: push the long tail, pull the short celebrity list, merge and rank at read time, return paginated with a cursor. Decide push vs pull by the read/write ratio and followers-per-author, and be ready to name the pitfalls: the celebrity threshold knob, backfill on follow, wasted pushes to inactive users, deletes/edits across copies, and thundering herd on cache miss.
Go deeper (optional): the canonical industry write-ups are the original Twitter "Timelines at Scale" talk and Instagram's engineering posts on their feed; both describe exactly this push/pull hybrid and the celebrity (hot-key) carve-out. You don't need them to answer the question — everything required is on this page — but they're useful if you want production war stories.