Notes
Listen
Requirements. A user can publish a post, and sees a home feed built from friends, pages they follow and groups they belong to. The feed is ranked by relevance, not purely by time, and scrolls endlessly. We'll leave out ads, comment threads, chat and media processing, which has its own design. Confirm with the interviewer that ranking is in scope, because it changes the shape of the answer.
Non-functional requirements. A page of feed should come back in under three hundred milliseconds at p ninety-nine. A new post should reach friends within about ten seconds, so eventual consistency is fine. One rule is strict: never show a post the viewer isn't allowed to see. And plan for degradation: if the ranking service is slow, fall back to recent posts rather than failing.
- in scope:
- • publish a post (text, link,
- photo reference)
- • home feed from friends,
- followed pages, groups
- • ranked, not just by time
- • infinite scroll
- out of scope:
- • ads, comments threads, chat
- • media processing details
- p99 feed page: under 300 ms
- new post visible to friends
- within ~10 seconds
- never show a post the viewer
- is not allowed to see
- available over consistent
- graceful degrade: if ranking
- is down, show recent posts
Ask scale
Assume a billion daily users loading the feed ten times a day: ten billion loads, about a hundred thousand requests a second, double that at peak. Half of users post once a day, so five thousand posts a second. Here's the number that matters: with an average audience of three hundred, pushing each post to every inbox is a hundred fifty billion inbox writes a day, about one and a half million a second. That's a lot, but it's small writes to a cache.
Can we afford an inbox per user? Store only a post ID and a score, sixteen bytes. Five hundred entries for a billion users is eight terabytes. At sixty-four gigabytes per node that's about a hundred twenty-five nodes, two hundred fifty with a replica. Entirely reasonable. The posts themselves are about a kilobyte each, five hundred gigabytes a day, which goes in a durable store, not memory.
DAU 1B
feed loads/day 10 each
10B / 100,000 s = 100K rps
peak x2 = 200K rps
posts/day 0.5 per user
500M / 100,000 = 5K wps
avg audience 300
inbox writes = 150B/day
= 1.5M/s entry = postId 8 B + score 8 B keep 500 newest per user 1B x 500 x 16 B = 8 TB 8 TB / 64 GB per node = 125 nodes, x2 replicas = ~250 cache nodes posts: 500M x 1 KB = 500 GB/day, ~180 TB/yr
Define API
The API is small. Publishing a post takes text, media references and an audience setting, like friends only or public. Reading takes a cursor and a limit and returns a page of fully rendered items plus the next cursor. A lightweight endpoint returns how many new posts arrived, for the 'new posts' pill. And the client reports which posts were actually seen, which feeds both deduplication and ranking.
POST /v1/posts
{ text, mediaRefs[], audience }
-> { postId }
GET /v1/feed?cursor=..&limit=10
-> { items[], nextCursor }
GET /v1/feed/new?since=..
-> { count } "new posts" pill
POST /v1/feed/seen
{ postIds[] } impressions Data model
Data model. Posts are written once and read by ID many times, so a key-value or wide-column store keyed by post ID fits. The social graph is a set of typed edges: friend, follows page, member of group. The inbox is a sorted set per user of post ID and score, capped at five hundred, and it's purely derived, so losing it costs speed, not data. Seen-state per user is a compact bloom filter.
post (postId, authorId, ts,
audience, body, media)
key-value by postId
edge (srcId, dstId, type, ts)
friend, follow, member
inbox userId -> sorted set
(postId, score), cap 500
seen userId -> bloom filter
counts postId -> likes, shares High-level design
The write path. The post service saves the post durably and only then publishes a post-created event to the stream. The fan-out service consumes it, asks the social graph for the audience, and adds the post ID to each audience member's inbox. Because the event is in a durable log, a crashed fan-out worker just resumes from its last offset.
The read path. The feed service pulls a few hundred candidate IDs from your inbox, plus posts from large pages pulled live. It sends them to the ranking service, which fetches features like how often you interact with each author, and returns scores. The feed service drops anything already seen or not visible to you, hydrates the top ten from the post store, and returns them with a cursor.
Deep dive: fan-out
Deep dive one: fan-out. Friend lists are usually capped at a few thousand, so friend posts are always safe to push. Groups and small pages are fine too. Pages with millions of followers are the celebrity case: don't push. Instead keep a short recent-posts list per big author, cached hot, and merge it in at read time. Also skip pushing to users who haven't logged in for a month; rebuild their inbox when they return.
Making fan-out safe. Partition the stream by author ID so one author's events stay in order. Adding to an inbox is idempotent, because a sorted set ignores a duplicate member, so retries can't double-insert. Deletes and audience changes publish an update event, but we never rely on that alone: the read path re-checks visibility, because a stale inbox entry must never leak a deleted or private post.
- friend posts: audience capped
- (a few thousand max) -> push
- small pages, groups -> push
- big pages (millions):
- no push; keep a per-author
- recent-posts list, pull and
- merge at read time
- idle 30+ days: skip push
- partition stream by authorId
- -> one author in order
- inbox add is idempotent:
- same postId twice = no-op
- delete or audience change:
- publish post.updated, and
- re-check at read time
- backpressure: fan-out lag is
- a metric, autoscale workers
Deep dive: ranking
Deep dive two: ranking. Think of it as a funnel. Start with about five hundred candidates. Filter out anything seen, blocked or not visible. A cheap model trims to a hundred, then a heavier model predicts how likely you are to like, comment or share each one, and combines those into one score. Finally, rules add diversity, like at most two posts from the same author in a row. All of this fits a budget of roughly a hundred milliseconds.
Where do features come from? Most are precomputed: how often you interact with this author, the post's age, how fast it's collecting reactions, which content types you engage with. They live in a key-value feature store updated by batch jobs and streams. Put a timeout on ranking. If it trips, fall back to recency order. And cache the ranked list for about a minute so scrolling to page two is consistent and cheap.
- 1 candidates ~500 from inbox
- + big pages
- 2 filter seen, blocked,
- not visible
- 3 light model ~500 -> 100
- 4 heavy model 100 -> scored
- 5 rules diversity: max
- 2 per author
- budget ~100 ms for all of it
- features (precomputed):
- viewer-author affinity
- post age, early engagement
- content type preference
- feature store: key-value,
- batch + streaming updates
- ranking timeout 120 ms:
- fall back to recency order
- cache ranked page ~1 min
Deep dive: freshness
Deep dive three: freshness. The client reports which posts were actually on screen, batched every few seconds. A bloom filter per user remembers them in very little space; a false positive only means one post gets skipped, which is acceptable. For the new-posts pill, count inbox entries newer than the session start. A cheap poll every minute is enough; you don't need a WebSocket for a feed.
- client batches impressions
- every few seconds
- seen set: bloom filter,
- rare false positive only
- hides one post
- new-posts pill: count inbox
- entries newer than session
- start; poll every 60 s or
- piggyback on other calls
Trade-offs
Trade-offs and failure modes. Push spends memory and write throughput to buy cheap reads; pull spends read latency to save writes; we mix both by audience size. An inbox shard can be lost and rebuilt from the graph and post store. Ranking can time out and fall back. Monitor fan-out lag, p ninety-nine page latency, ranking timeouts, and engagement metrics like click-through rate, because a ranking bug shows up there first.
At ten times the scale, lower the threshold so fewer authors are pushed, shard inboxes by user ID with consistent hashing, and run feed stacks per region with posts replicated asynchronously between them. Use a cheaper first-stage model and trim inboxes. And state the big trade: we gave up storage and a few seconds of freshness in exchange for fast reads and high availability.
- hot author: graph lookup and
- fan-out spike -> pull mode
- inbox shard lost: rebuild
- from graph + post store
- ranking slow: recency fallback
- privacy bug: always filter at
- read, not only at write
- monitor: fan-out lag, p99
- page, rank timeouts, CTR
- lower the push threshold
- shard inboxes by userId,
- consistent hashing
- regional feed stacks; replicate
- posts across regions async
- cheaper first-stage model
- trim inboxes to 300 entries
- traded: storage + freshness
- for read speed + uptime
Say it like this
The summary. Posts are stored durably, then an event drives fan-out that pushes post IDs into per-user inboxes for friends and small pages, while big pages are pulled at read time. A feed request takes candidates from the inbox, filters seen and invisible posts, ranks them in two stages with a diversity pass, then hydrates the top results. Privacy is enforced at read time, and recency is the fallback.
- post -> durable store -> event
- fan-out pushes IDs to inboxes
- (friends, small pages)
- big pages pulled at read
- read: candidates -> filter ->
- light model -> heavy model
- -> diversity -> hydrate
- privacy checked at read
- fallback: recency if slow