Courses / System Design Interview / Chapter 29
Module 3 · Design Real Products · Chapter 29 of 39
Design a News Feed
Builds on Back-of-the-Envelope Estimates, Abstractions That Hide the Network, Content Delivery Networks, Distributed Caches, Message Queues, Publish-Subscribe, Sharded Counters, Design a Video Streaming Platform and Design a Microblog. Next up: Design a Photo-Sharing App.
Part 1 · Requirements and Scale
The brief
Diwali morning, 8 am. Meena opens the app for the first time since last night, and her 400 connections have posted about 2,000 things since then: her cousin Asha’s wedding photos from 3 am, forty match updates from cricket pages, sale posts from shops she followed years ago. In newest-first order, Asha’s photos sit on screen nine, and Meena never gets there. Last year the team fixed that by re-ranking on every scroll. Page 2 then repeated three posts from page 1, and at the 9 pm peak the ranking fleet ran out of capacity and feeds stopped loading. This chapter builds a feed that ranks well, pages cleanly and stays up.
The product: the news feed of a social app with 300 million daily users. People see posts from friends, pages and groups, ordered by what they are most likely to care about, and the feed keeps loading as they scroll. You are designing how the feed is gathered, ranked and paged.
From timeline to feed
Design a Microblog built a timeline: posts from accounts you follow, newest first. A news feed draws on the people, pages and groups you are connected to, but orders posts by how likely you are to care about them. It mixes text, photos, videos and links, and it keeps loading as you scroll.
Think of a friend who reads every newspaper in town and hands you the five stories you would actually want. That friend is the ranking, and in a feed ranking is the feature. Three things make it harder than a timeline. The order depends on the viewer, so no two people share a list. It depends on signals that change by the minute, such as how many likes a post has collected. And Meena still expects her scrolling to stay stable while all of that moves underneath.
The scope below keeps ranking and paging at the centre.
| In scope | Out of scope |
|---|---|
| A ranked feed of posts from friends, followed pages and groups | Creating posts: a post service already exists |
| Infinite scroll with no repeats and no gaps | Choosing and inserting ads (leave a slot for them) |
| Posts with text, photos, videos and link previews | Comments and messaging |
| A "new posts" prompt when fresh posts arrive | Training the ranking model; we call it as a service |
| Hide a post, or see less from an author | Notifications |
The items on the right either exist already or plug into fixed slots, so none of them changes how the feed is built.
Many apps offer a "latest" view beside the ranked one. That view is the microblog timeline with a post id as its cursor; everything in this chapter is about the ranked default.
How well it must work
A feed is judged on feel: the first screen must appear quickly and scrolling must never stall. The requirements turn those feelings into numbers.
- Fast first page. The server returns the first 10 posts in under 300 ms at p99, the time that 99 of every 100 requests beat; images then load from a nearby CDN.
- Smooth scrolling. Each later page returns in under 100 ms, so the next screen is ready before the thumb gets there.
- Fresh enough. A friend’s new post becomes eligible for your feed within about a minute.
- Degrade, do not fail. If ranking is slow or down, show a feed in time order rather than an error.
- Privacy is checked at read time. A post shared with friends only must never reach someone who has since been unfriended.
The assumptions below drive every estimate.
| Assumption | Value |
|---|---|
| Daily active users | 300 M |
| Feed sessions per user per day | 8 |
| Pages per session (10 posts each) | 3 |
| New posts per day | 50 M |
| Connections per user (friends, pages, groups) | 400 on average |
Together they give the request rate, by the method of Back-of-the-Envelope Estimates: 300 M × 8 sessions × 3 pages = 7.2 B page loads a day, about 83,000 a second on average and roughly 250,000 a second around the 9 pm peak (3×). The next page asks whether the precomputed data behind those pages fits in memory.
Estimates: memory for candidate pools
A ranked feed needs something to rank. For each user we keep a candidate pool: recent posts from their connections that they have not seen yet. Meena’s pool holds the posts her 400 connections made since she last looked. Pools are kept up to date as posts are created and ranked from at session start, so first check that they fit.
Try it first: each pool keeps 500 entries of 32 bytes (post id, author id, time, affinity, flags) for each of 300 M daily users, with two copies. How much memory is that?
- 500 × 32 B = 16 KB per user.
- 16 KB × 300 M = 4.8 TB for one copy.
- × 2 copies ≈ 10 TB: about 150 servers with 64 GB free each, before growth.
Ten terabytes of memory is affordable at this size but not free. Keep in memory only the pools of people who open the app most days, and move the rest to an SSD-backed store, where a read costs a millisecond or two instead of a fraction of one. A pool nobody has read for a week can be dropped and rebuilt on the next visit.
The other estimates fill in the rest of the load.
| Quantity | Working | Result |
|---|---|---|
| Feed page loads | 7.2 B a day ÷ 86,400 s | ≈ 83,000 /s, peak ≈ 250,000 /s |
| Post lookups to fill pages | 83,000 pages × 10 posts | ≈ 830,000 /s, nearly all from cache |
| Impression events | 7.2 B pages × 10 posts = 72 B a day | ≈ 830,000 /s, the biggest write stream |
| Candidate inserts | 50 M posts × 400 connections ÷ 86,400 s | ≈ 230,000 /s |
| Media bandwidth | 83,000 pages × 5 images × 150 KB | ≈ 62 GB/s, about 500 Gbps on average |
| Scoring work | 2.4 B sessions × 500 candidates ÷ 86,400 s | ≈ 14 M scores /s, if we rank once per session |
Two rows shape the design: scoring work, which decides when ranking runs, and media bandwidth, which must go through Content Delivery Networks, never through feed servers.
Part 2 · API, Data and the Pipeline
The API
With the load known, the API stays small: fetch a page, ask whether anything new has arrived, and report what the user saw or hid. All the ranking happens behind these calls.
GET /v1/feed?limit=10 -> 200 {"items": [...], "next_cursor": "c2Vzcz..."} GET /v1/feed?limit=10&cursor=c2Vzcz... -> 200 {"items": [...], "next_cursor": "..."} GET /v1/feed/new-count?cursor=c2Vzcz... -> 200 {"count": 4} POST /v1/feed/impressions {"post_ids": ["p81", "p77"], "session": "s_3f"} -> 204 POST /v1/feed/hide {"post_id": "p81", "reason": "see_less_from_author"} -> 204 PUT /v1/posts/{post_id}/reactions/me {"type": "like"} -> 204
Each item carries the post, its author, counts and media URLs that point at the CDN.
The cursor is opaque to the client: the server packs into it everything it needs to continue the same ranked list, which the paging deep dive explains. Impressions, the record of which posts actually appeared on screen, go in batches every few seconds rather than one call per post. They feed ranking and stop the app from showing Meena the same post again tomorrow.
Each item in a page carries everything the phone needs to draw one card:
Media URLs are sized for the screen. A phone 1080 pixels wide gets the 1080-pixel copy, about 150 KB, not the 4000-pixel original, and private posts get signed CDN URLs that expire after a few minutes. Videos start muted at a low quality from short segments, as Design a Video Streaming Platform describes. The next page covers the data behind these calls.
Data model
The feed reads data that other services own: posts, people and the connections between them. It adds two things of its own, the candidate pool per user and an event log of what users did.
user_id (pk)nameavatar_keypost_id (pk)author_idaudience (public, friends, group)type (text, photo, video, link)media_keyscreated_atuser_id (pk part 1)target_id (pk part 2)kind (friend, page, group)affinityupdated_atuser_id (pk part 1)post_id (pk part 2)author_idaffinityinserted_atseenuser_idpost_idaction (view, like, comment, share, hide)at- posts author_id → users
- connections user_id → users
- candidate_pool user_id → users
- candidate_pool post_id → posts
- interactions post_id → posts
Each kind of data goes where its access pattern fits:
| Data | Store | Why |
|---|---|---|
| Posts | Sharded key-value store with a post cache | Read by id, close to a million times a second |
| Connections and affinity | Wide-column or graph store, keyed by user | One read per feed build: all of my connections |
| Candidate pools | In memory for active users, SSD-backed for the rest | Many small appends; read when a session starts |
| Interactions | Append-only log, then a data warehouse | Huge volume; used to compute signals and train models |
| Engagement counts | Sharded counters | A viral post gets thousands of reactions a second |
Only the pools are written on every new post; everything else is read-mostly or append-only. Affinity is a number from 0 to 1 for each pair of user and connection: how close they seem, from likes, comments, messages and profile visits over the last few weeks. Meena’s affinity to Asha is about 0.9. It changes slowly, so a daily batch job over the interactions log is enough.
A pool has a simple life cycle:
- Fills: a fan-out worker appends a post id when one of your connections posts.
- Drains: impressions mark entries as seen, and entries older than about three days expire.
- Stays small: once a pool holds 500 entries, the lowest-affinity entries go first, so someone with 5,000 connections keeps the people closest to them.
High-level design
Those pieces form a pipeline. The first stage runs when posts are created; the rest run when Meena opens the app. Pools fill exactly as Design a Microblog fills timelines: fan-out on write for most authors, pull at session start for pages above a follower threshold, chunked jobs and all. This chapter does not repeat that machinery; its depth goes to what happens after the pool is full.
- 1Gather candidatesher pool, big pages pulled now, recommended and trending posts
- 2Filterdrop seen, hidden, deleted and no-longer-visible posts
- 3Fetch signalsaffinity, post age, engagement so far, content type
- 4Score and re-ranka score per post, then rules for variety
- 5Cache and servestore the ranked list for this session; return 10 posts at a time
The scene follows Meena’s first page, her second, and an evening when ranking falls behind.
8 am: Meena opens the app, and no ranked list exists for this session yet
Read it as steps
Watch for the 150 ms timeout: the morning page is ranked and cached, and at the 9 pm peak the feed still loads in time order.
The first page pays for ranking once, and later pages are cache reads. When ranking misses its budget, the feed falls back to time order instead of failing, and feed servers send small JSON while the CDN sends the heavy bytes, so 500 Gbps of media never needs 500 Gbps of feed servers.
Where candidates come from
The pipeline’s first stage decides what can be ranked at all. So far every candidate came from the viewer’s connections; most feeds mix in other sources, each producing a short list at session start that joins the pool before ranking.
| Source | What it adds | How it is built |
|---|---|---|
| Connections | Posts from friends, followed pages and groups: most of the feed | Fan-out into the pool, as in Design a Microblog |
| Recommended | Posts from accounts the viewer does not follow but may like | A recommendation service returns a few hundred, matched by topic or by what similar people engaged with; refreshed every few hours |
| Trending nearby | What many people in the region are engaging with now | A stream job keeps the top posts per region and language: one shared list, refreshed every few minutes |
| Ads | Paid posts in fixed slots | The feed reserves positions, say 4 and 12; the ads system fills them when the page is served |
Each candidate carries its source, and the ranker scores all of them on the same scale. Re-ranking rules then cap each source, for example at most 3 recommended posts in every 10, so Asha is never crowded out by strangers.
Cold start is the empty-pool problem. A new user has no connections, no affinity and no history, so start with trending posts in their region and language, ask for a few interests at sign-up, and suggest people from their contacts if they allow it. Their first likes and hides reshape the very next session. A new post has the same problem: with few impressions its engagement rate means little, so show it to a small sample first and widen if people respond. Those impressions are the next page’s subject.
Impressions: the biggest write stream
Ranking learns from what people saw, so every post that appears on screen produces an impression event. 7.2 B pages a day × 10 posts is 72 B impressions, about 830,000 events a second, the biggest write stream in the system. New posts arrive at under 600 a second; impressions arrive more than a thousand times faster.
Nobody reads these events one by one, so they never touch a database. The impressions endpoint checks a batch, appends it to a partitioned, replicated log, as in Publish-Subscribe, and returns 204. Phones send one batch every few seconds, so the endpoint handles requests, not single events. Three consumers read the log at their own pace:
| Consumer | What it does | What it feeds |
|---|---|---|
| Seen marker | Marks pool entries as seen, per user | The next session skips them |
| Stream counter | Counts impressions per post in one-minute windows | The engagement rate’s denominator, in the signal store within seconds |
| Warehouse loader | Writes hourly files to the data warehouse | Daily affinity jobs, model training and A/B test results |
One write feeds three jobs, and none of them slows the phone. Size the log from bytes, not events: at about 100 bytes an event, that is roughly 83 MB/s on average and 250 MB/s at the evening peak. One log partition handles about 10 MB/s, so the peak needs 25 partitions; plan for about 50 to keep 2× headroom for spikes and replays. Key them by user id, so one user’s seen marks stay in order on one partition.
A phone resends a batch when the reply is lost, so events arrive at least once. Give each batch an id and let the counter skip ids it has seen in the last hour. A count off by a fraction of a percent never changes a ranking.
Part 3 · Deep Dives
Precompute or rank on request
Speed, freshness and cost all come back to this one choice: when the expensive scoring work runs, and how many times.
Think of a restaurant. It can cook every dish before opening, cook each plate when ordered, or prepare ingredients in the morning and cook when the table sits down. A feed has the same three choices, and the choice decides latency, freshness and the bill.
| Rank everything offline | Rank on every request | Pool on write, rank per session | |
|---|---|---|---|
| When ranking runs | A batch job every few hours | Each page load | When a session starts |
| Latency per page | Lowest: every page is a cache read | About 100 ms of scoring on every page | About 100 ms on the first page, then cache reads |
| Freshness | Hours old | Up to the second | Fresh at session start |
| Wasted work | Feeds built for people who never open the app | The same posts re-scored on every scroll | Little |
| Scoring work | Every user every few hours, visit or not | ≈ 42 M scores a second | ≈ 14 M scores a second |
Rank everything offlineA batch job every few hours
Rank on every requestEach page load
Pool on write, rank per sessionWhen a session starts
Rank everything offlineLowest: every page is a cache read
Rank on every requestAbout 100 ms of scoring on every page
Pool on write, rank per sessionAbout 100 ms on the first page, then cache reads
Rank everything offlineHours old
Rank on every requestUp to the second
Pool on write, rank per sessionFresh at session start
Rank everything offlineFeeds built for people who never open the app
Rank on every requestThe same posts re-scored on every scroll
Pool on write, rank per sessionLittle
Rank everything offlineEvery user every few hours, visit or not
Rank on every request≈ 42 M scores a second
Pool on write, rank per session≈ 14 M scores a second
The third column wins for most products. Keep pools up to date as posts are created, rank once when a session starts (roughly one sitting with the app), cache that ranked list for about 15 minutes, and serve every scroll from it. There are 7.2 B page loads but only 2.4 B sessions a day, so this does a third of the scoring of ranking per page, and it never reshuffles posts under Meena’s thumb.
The advice flips at the edges. A "latest" view needs no ranking at all. A short-video app where each swipe changes what the viewer wants may re-rank every few items, and pays for it. And users who open the app every morning at 8 can have their first list built a few minutes earlier, so their first page is instant, while monthly visitors get theirs on demand. The next page sets the budget for that per-session work.
Ranking once per session: the budget
Ranking per session moves the cost to one moment, the first page, so that moment needs a budget. 2.4 B sessions a day is about 28,000 session starts a second, and about 83,000 at the 9 pm peak; at 500 candidates each, that is about 42 M scores a second at the peak.
The first page must return in 300 ms at p99, and each step gets a share of it:
| Step on the first page | Typical time | Limit |
|---|---|---|
| Gather: read the pool, pull big pages, add recommended posts | 20 ms | 50 ms |
| Fetch signals for 500 candidates | 30 ms | 50 ms |
| Score 500 candidates | 100 ms | 150 ms timeout |
| Re-rank, cache the list, hydrate 10 posts | 20 ms | 30 ms |
| Total on the server | ≈ 170 ms | under 300 ms with room for the network |
Scoring takes the largest share, so it gets a hard timeout. If it runs out, serve the candidates in time order and rank properly on the next session: a plain feed now beats a perfect feed in three seconds. The cached list itself is cheap: 500 ids × 8 bytes = 4 KB a session, and 83,000 starts a second kept for 15 minutes is about 75 M lists, roughly 300 GB.
Three cases need rules. A user who reopens within 15 minutes continues the same list. A pull to refresh re-ranks, but at most once a minute per user, so an impatient thumb cannot multiply the load. And new candidates that arrive mid-session wait behind a "new posts" prompt rather than reshuffling the list.
Larger systems score in two passes: a cheap model trims 500 candidates to about 100, and a heavier model orders those. If the heavy model costs ten times as much per candidate, that is 500 + 1,000 = 1,500 units instead of 5,000, about 70% less.
Ranking: signals and a score
The budget says when scoring runs; this page says what it computes. Each candidate gets a score from a handful of signals, measurable facts about the viewer, the author and the post, and the feed shows the highest scores first. Five signals do most of the work:
- Recency: how old the post is.
- Affinity: how close the viewer is to the author, such as 0.9 for Meena and Asha.
- Engagement: how others reacted, relative to how many saw it.
- Content type: what this viewer tends to engage with, such as videos watched to the end.
- Negative feedback: hides, "see less" and reports, which count against.
Engagement must be a rate, not a count. 120 likes from 1,000 viewers (12%) beats 2,000 likes from 100,000 viewers (2%), because the first post is landing with the people who see it. The rate is compared with other recent posts to get a value from 0 to 1, and a post with only a few views gets a neutral value until more arrive. That rate is why the impressions stream exists.
A simple, explainable score multiplies a weighted sum of signals by a time decay, so every post loses half its score every 6 hours:
score = (0.5 × affinity + 0.3 × engagement + 0.2 × type_match) × 0.5 ^ (age_hours / 6)
Here are three posts from Meena’s 8 am pool:
| Post | Affinity | Engagement | Type match | Age | Score |
|---|---|---|---|---|---|
| A. Asha’s wedding photos | 0.9 | 0.4 | 0.7 | 5 h | 0.71 × 0.56 = 0.40 |
| B. A cricket page’s viral video | 0.2 | 0.95 | 0.9 | 2 h | 0.565 × 0.79 = 0.45 |
| C. An acquaintance’s text post | 0.3 | 0.1 | 0.4 | 20 min | 0.26 × 0.96 = 0.25 |
Affinity0.9
Engagement0.4
Type match0.7
Age5 h
Score0.71 × 0.56 = 0.40
Affinity0.2
Engagement0.95
Type match0.9
Age2 h
Score0.565 × 0.79 = 0.45
Affinity0.3
Engagement0.1
Type match0.4
Age20 min
Score0.26 × 0.96 = 0.25
Time order would show C, B, A, with Asha last. The score shows B, A, C: the fresh but dull post drops, and Asha’s photos move up despite being five hours old. Change the weights and the feed’s personality changes, which is why new weights go to a small share of users first. Re-ranking then adds variety: no two posts in a row from one author, and a cap on videos per page.
Why offsets break
A ranked list is only useful if Meena can scroll through it cleanly, and feeds change while she reads: new posts arrive, scores move, and she hides things. Offset pagination, "give me items 11 to 20", counts positions in a list that will not hold still.
Predict before you watch: Meena sees posts A to J on page 1. Then 3 new posts rank in at the top. With offset paging, what does page 2 (items 11 to 20) start with?
- H, I and J again: everything shifted down by three, so items 11 to 13 are posts she already saw.
Offset paging: page 1 is items 1 to 10, posts A to J
Read it as steps
Watch page 2 in both runs: offsets serve H, I and J again after new posts arrive, while the cursor reads on from the frozen list.
Offsets count positions in a list that keeps moving, so new posts cause repeats and removed posts cause silent gaps. A cursor names a position in one frozen list, so later changes cannot move it. The next page shows what goes inside that cursor.
Cursors into a frozen list
A cursor is an opaque token that says where you are in a particular list. In the microblog’s timeline it can be the last post id shown, because that list is sorted by id. A ranked list is not. The last score shown fails too: scores change between pages, and ties make "after 0.45" ambiguous. So the cursor holds the id of the session’s ranked list and a position in it.
cursor = base64( sign({"list": "L7", "pos": 10, "user": "u42", "expires": "2026-10-16T09:15Z"}) )The server signs it so clients cannot edit it or read another user’s list, and because clients never look inside, the server can change its contents later without breaking old app versions.
Lists expire, so the cursor must survive that. If list L7 is gone when page 2 is requested, the server ranks a new list and skips any post ids already shown in this session, which it keeps in a small seen set: about 30 ids for three pages, a few hundred bytes. Meena sees no repeats, even across a rebuild.
Freshness works with the frozen list, not against it. While the app is open, it calls new-count every minute or two and shows "4 new posts". It inserts them only when Meena taps or pulls to refresh, which starts a new list at position 0, because reshuffling under someone’s thumb loses their place.
The app should also drop any post id it has already rendered. It costs a few lines of code and hides any server-side slip.
With ranking and paging in place, the next question is what happens when the parts they depend on fail.
Part 4 · Failures and Trade-offs
Bottlenecks and failures
Every dependency of the feed can be slow or down, and the rule for each is the same: return something useful within the time budget. The table lists each failure, what the user sees, and how the design copes.
| What goes wrong | What the user sees | How the design copes |
|---|---|---|
| Ranking slow or down, or signals missing | A slightly plainer feed | Defaults for missing signals; a 150 ms timeout, then candidates in time order |
| Feed cache node lost at the 9 pm peak | A slower first page for some users | Re-rank from the pools on demand; coalesce repeated requests for the same list, as in Distributed Caches, and rate-limit rebuilds |
| A candidate pool node fails | Nothing, or a plainer feed for a while | Its second copy serves reads; a lost pool is rebuilt by pulling recent posts from the user’s connections |
| A post goes viral | Nothing, if handled | The post object is replicated in the cache; counts come from Sharded Counters, refreshed every few seconds |
| Fan-out falls behind after a big match | New posts appear late | Handled as in Design a Microblog: more workers, active users first; the "new posts" prompt hides the delay |
| Someone is unfriended | Their friends-only posts must vanish | Visibility is checked at hydration, never trusted from the pool |
| The impressions log is slow or down | A few seen posts may return next session | Phones keep batches and resend them; ranking uses the last counts; consumers catch up |
| A zone or a whole region fails | A zone: nothing visible. A region: a slower, plainer first page | Pools and caches keep a replica in another zone. A failed region’s users move to another, which rebuilds pools at a capped rate and serves time order until it catches up |
Almost every failure ends in the same place, a plainer feed in time order, which is why that fallback is designed in from the start rather than bolted on.
Trade-offs, quality and cost
A feed that never fails can still fail its users. The big product trade-off is engagement against variety: a feed tuned only for clicks fills up with one kind of post. The re-ranking rules help, and so do a few slots for newer authors, so the system keeps learning what Meena likes.
Clicks alone are a poor target because they reward bait, so every ranking change runs as an A/B test: a small random share of users gets the new version, and a mix of metrics is compared with everyone else before it rolls out.
| Metric | What it tells you |
|---|---|
| Sessions per user per day | Whether people come back |
| Hides and "see less" per 1,000 impressions | Whether the feed annoys people |
| Share of posts from close connections | Whether friends get buried under pages |
| Retention after 30 days | The long-term effect that short-term clicks can hide |
A change that lifts clicks but also lifts hides, or lowers 30-day retention, is not shipped. In production, three signals show whether the system itself is healthy:
Media egress through the CDN dominates the bill: about 500 Gbps on average, some 160 PB a month. Ranking compute comes second, which is why ranking once per session instead of once per page matters.
Part 5 · 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 | Ranked feed from friends, pages and groups; infinite scroll with no repeats; a new-posts prompt. Out: creating posts, choosing ads, training models. | First page p99 under 300 ms; later pages under 100 ms |
| 5–8 | Page loads, impressions and scoring work. | 83,000 pages/s, 250,000 at peak; 830,000 impressions/s |
| 8–13 | GET /feed with an opaque cursor; impressions in batches. Candidate pools, connections with affinity, an interactions log. | 500 candidates × 32 B per user ≈ 10 TB with two copies |
| 13–22 | Fan-out fills pools (as in the microblog); at session start: other sources, filter, signals, score, re-rank, cache the list. Hydrate from the post cache; media from the CDN; impressions to a log. | Rank once per session; keep the list 15 minutes |
| 22–37 | When ranking runs and its budget, then the score, then cursors into the frozen list. | Per session: 14 M scores/s, not 42 M; 150 ms timeout |
| 37–40 | Ranking down, a region failing, what you monitor, engagement against variety. | Alert on the fallback rate |
Pick the deep dive from the interviewer’s questions. Latency or cost: when ranking runs. Why one post beats another: the score and its signals. Infinite scroll or duplicates: cursors. A brand-new user: candidate sources and cold start.
- Say
- "I keep a pool of about 500 candidates per user, filled by fan-out, and rank it once per session: 14 M scores a second instead of 42 M."
- Say
- "The cursor points into the session’s ranked list, frozen for 15 minutes, so new posts wait behind a prompt instead of shifting pages."
- Say
- "If ranking misses its 150 ms budget, I serve the pool in time order: a plainer feed beats an error."
- Avoid
- "A machine learning model decides." Name the signals and say what the score does with them.
- Avoid
- "Page 2 is items 11 to 20." In a feed that re-ranks every minute, offsets repeat and skip posts.
Interview drill
Interviewers often change the problem halfway to see whether the design bends or breaks. Answer each one before opening it.
A cricket final ends at 11 pm and feed opens jump to 10× normal for 15 minutes. What breaks, and what do you change?
- Session starts carry the cost: ranking each one fresh would mean about 140 M scores a second.
- Reuse a ranked list when a user reopens within minutes, and pre-rank for the most active users before known events.
- Shed quality before failing: only the cheap first-pass model, or time order for part of the traffic.
Five friends share the same article. Product wants one card: "Asha and 4 others shared this". What changes in the data model?
- Key pool entries by the root post or the link’s canonical URL: (user_id, root_id, sharers, latest_at).
- A reshare updates the existing entry instead of appending a new one; the number and closeness of sharers become a signal.
- Seen sets and cursors use root_id, so the article does not return as another friend’s share.
Finance wants ranking compute cut in half without hurting feed quality. What do you change?
- Rank in two passes: a cheap model trims 500 candidates to about 100 for the heavy model.
- When a user returns within 30 minutes, score only the new candidates and merge them in.
- Prove it with an A/B test: hides per 1,000 impressions and 30-day retention must hold, not just clicks.
- Common miss
- Ranking on every page. It triples scoring work and reshuffles posts under the user’s thumb; rank per session and page through the cached list.
- Common miss
- Ignoring the write side. Impressions, about 830,000 a second, outnumber every other write; they belong in a log, not a database.
Wrap-up
Use this page to revise before a mock round.
Next, Design a Photo-Sharing App reuses this feed and moves the weight to the upload side: signed URLs, resize workers, a safety scan and petabytes of images.
Part 6 · Check yourself
Quick check
Q1A teammate’s feed re-ranks on every request and pages with ?offset=10. Users report seeing posts from page 1 again on page 2, mostly in the evening. What is the cause, and the fix?
Think it through, then check.
- New posts and changing scores shift the list between requests, so offset 10 points at what used to be about item 8; evenings bring the most new posts.
- Fix: rank once per session, freeze the list, hand out a signed cursor (list id, position), and keep a seen set for rebuilds.
Q2Product wants 30% of each feed to come from accounts the user does not follow. What changes?
Think it through, then check.
- New candidate sources: recommended posts by topic and similar users, plus trending posts nearby; only public posts qualify.
- Unfollowed authors have no affinity, so their score leans on topic match, engagement rate and past hides; a quota keeps about 3 of every 10 slots.
- About 200 more candidates per session add roughly 40% to scoring: check the 150 ms budget, and A/B test the hide rate.
Q3A teammate wants to rank engagement by raw like counts, because they need no impression data. What do you gain, and what do you lose?
Think it through, then check.
- Gain: simpler signals; like counts already exist in the sharded counters.
- Lose: reach beats quality, so a page’s post liked by 2,000 of 100,000 viewers outranks Asha’s photo liked by 120 of 1,000.
- The saving is small: impressions are collected anyway to mark posts seen.
Q4At 9 pm the ranking service is overloaded and most calls time out. What does Meena see, and what stops the overload from getting worse?
Think it through, then check.
- A feed in time order after the 150 ms timeout, not an error.
- No retries of ranking inside the request: retries multiply the load on a service that is already drowning.
- A circuit breaker (Abstractions That Hide the Network) sends everything to the fallback while most calls fail, then lets a few through to test recovery; alert on the fallback rate.
Q5Which building block carries impressions from phones to ranking and the warehouse, and why not a database table?
Think it through, then check.
- A partitioned, replicated log, as in Publish-Subscribe: appends stay cheap at 830,000 events a second.
- Several consumers read it at their own pace: the seen marker, the stream counter and the warehouse loader.
- It keeps a few days for replay after a bug; a table would index 72 B rows a day that nobody reads one by one.