Scope the requirements
The interviewer is listening for one thing above all: do you see that a feed read is potentially hundreds of scattered queries, and do you move that work somewhere cheaper? Everything else — celebrities, viral posts, pagination — is a consequence of how you answer that.
Functional — what it must do
- Users create posts (text is enough for v1).
- Users follow other users — asymmetric, no approval.
- Users see a feed of posts from people they follow, reverse-chronological.
- Cursor-based pagination through the feed, 20 or so items per page.
- Explicitly cut: likes, comments, ML ranking, privacy circles — I name them and offer them back as extensions.
Non-functional — the numbers that shape the design
- Feed load p99 under 500 ms — this is the app's home screen.
- A new post is visible to followers within 1 minute — eventual consistency is fine, availability wins over consistency.
- Heavily read-skewed: roughly 40 feed loads for every post created.
- Follower counts are unbounded — a 100M-follower account must not break the design.
- Scale target: 200M DAU, ~2B feed loads per day.
I'll design for availability and a one-minute staleness budget, because nobody notices a post arriving 30 seconds late but everyone notices a feed that takes 2 seconds to load. That staleness budget is what lets me do expensive work asynchronously.
Back-of-envelope estimates
The arithmetic here isn't decoration — the fan-out multiplication is the single number the whole design pivots on, so I do it out loud.
| Quantity | Estimate | How you got there |
|---|---|---|
| Posts (writes) | ~600/s | 50M posts/day ÷ ~86k s; peak ~3k/s. |
| Feed loads (reads) | ~20k/s | 2B loads/day; peak ~100k/s. Reads outnumber posts ~40:1. |
| Fan-out writes | ~100k/s | 50M posts × ~200 avg followers = 10B feed-cache inserts/day — the write amplification I'm signing up for. |
| Feed cache per user | ~2 KB | 200 post IDs × ~10 B each. Deliberately small — older pages fall back to a pull path. |
| Total feed cache | ~1 TB | 2 KB × 500M cached users. Even at 2B users it's ~4 TB — a modest Redis cluster. |
| One celebrity post | 100M writes | 1 post × 100M followers. This single row is why the design goes hybrid. |
Two numbers matter: reads beat writes 40 to one, which says precompute feeds at post time — and one celebrity post costs 100 million cache writes, which says I can't precompute for everyone. The design has to hold both at once.
Sketch the API
Three endpoints carry the product. The interesting decision is the pagination cursor, because a feed is a list that changes while you're reading it.
- Cursor pagination, never offset: with offset, a post arriving between page 1 and page 2 shifts everything down and the reader sees duplicates. A cursor of
(createdAt, postId)is stable under inserts — page 2 is "everything older than the last item I saw", regardless of what arrived since. - Follow is a PUT, not POST, deliberately — it's idempotent state ("A follows B"), so retries and double-taps can't create duplicate edges.
Data model
Three narrow tables. Every read-time access is a point or range lookup by a known key — no joins — and that shapes the database choice.
At 200M DAU I'd put posts and follows in DynamoDB or Cassandra — the access patterns are pure key-value, there are no read-time joins, and I get partitioning without operating shards myself. If the interviewer pins scale to a few million DAU, I'd say so and run sharded Postgres happily; the schema doesn't change, only who does the partitioning.
High-level design
Two paths with opposite shapes. The write path is slow and wide — one post fans out to thousands of caches through a queue. The read path is narrow and fast — one Redis fetch, one hydration, done.
- Author POSTs → post service inserts the row, drops a post event on Kafka, and returns. The author waits for one database write, nothing else.
- Fan-out workers consume the event, read the author's follower list, and append the postId to each follower's Redis feed list, trimming each to ~200 entries.
- Reader GETs /feed → feed service fetches their ~200 IDs from Redis in one call.
- It hydrates the IDs into full posts from a post cache (Redis, cache-aside) with the database behind it, and merges in celebrity posts pulled live for any flagged accounts the reader follows.
- Response goes back with a
(createdAt, postId)cursor for the next page — well inside the 500 ms budget because nothing scattered.
Deep dives — where the interview is won
Fan-out on write wins because reads outnumber posts 40 to one
Fan-out on read is the naive shape: at feed-load time, fetch the reader's followee list, query each followee's recent posts, merge and sort. For someone following 500 accounts that's hundreds of index queries scattered across partitions, on every single load, 20k times a second — the p99 dies on the slowest shard and the 500 ms budget is gone. Fan-out on write inverts it: do the expensive work once at post time, when nobody is waiting, and make the read a single list fetch.
The mechanics: post insert → event on Kafka → workers read the follower list and append the postId into each follower's cached feed. The write amplification is real — 200× on average, 10B appends a day — but it's async, it's absorbed by a queue, and my one-minute staleness budget is exactly what pays for it. This trade only works because the requirement said eventual; I make that connection explicitly, because it shows the SLO drove the architecture rather than the other way round.
Celebrities break push, so flag them and pull
One account with 100M followers turns a single post into 100M cache writes — that one task takes minutes, stalls the queue behind it, and most of the work lands in feeds nobody is currently reading. No amount of worker scaling makes this sensible; the strategy itself is wrong for that account shape.
The fix is a hybrid, chosen per account, not globally: accounts above a follower threshold (say 100k) are flagged no-precompute. Their posts skip fan-out entirely. At read time, the feed service checks which flagged accounts the reader follows — a short list for almost everyone — queries those authors' recent posts directly via the (authorId, createdAt) index, and merges them with the precomputed list before returning. Readers follow few celebrities, so the merge is a handful of well-indexed queries, not the hundreds that pure pull would cost.
A viral post melts one cache shard — replicate it, don't shard harder
Hydration reads go through a post cache sharded by postId. That's fine until one post goes viral: consistent hashing puts it on exactly one shard, and that shard now takes millions of reads a second while its siblings idle. Adding shards does nothing — the hot key still lives in one place. This is the classic hot-key problem and interviewers ask it here deliberately.
The answer is to replicate hot posts instead of sharding them: detect hot keys with a cheap counter, then hold the viral post on every cache instance and let the load balancer spread reads across all of them. Reads for that key now scale with the number of cache nodes rather than being pinned to one.
The feed cache holds 200 entries, and that's a feature
Why not cache the whole feed? Because 200 IDs at ~2 KB per user keeps the entire cache around a terabyte, and almost nobody scrolls past page 3. When someone does page past the cached window, the feed service falls back to the pull path — query followees' timelines directly and merge. Slow, rare, acceptable; I say the fallback exists so the 200 doesn't look like a bug.
The bigger saving is skipping fan-out for inactive users: anyone dormant for 30+ days gets no precomputed feed at all, which cuts the fan-out write volume by well over half since most registered users aren't active. On their next login, the feed is rebuilt once by pull and precompute resumes. Same idea, applied to time instead of depth: don't do work for readers who aren't reading.
Follow-ups you should expect
A user unfollows someone, or a post is deleted — but it's already in thousands of precomputed feeds. Now what?
Why not a graph database for the social graph?
How would you move from chronological to a ranked feed?
What happens when the fan-out queue backs up?
How do you keep pagination stable while new posts keep arriving?
Where candidates lose the room
- Fanning out synchronously in the POST handler — the author's request now waits on thousands of writes, and a celebrity post times out. The queue is the design.
- Picking pure push or pure pull and defending it — the celebrity question is coming either way, and the answer is per-account hybrid, not a global strategy.
- Offset pagination on a list that changes under the reader — duplicates and skips on page 2 are the immediate, demonstrable bug.
- Reaching for a graph database because the domain says "social graph" — the access patterns are two KV lookups; the fancy tool signals pattern-matching, not judgement.
- Treating fan-out tasks as uniform queue items when they vary by six orders of magnitude — one viral post stalls the partition and every small post behind it goes stale.