Skip to the solution
HardSystem Design16 min read

How would you design a news feed system?

1.2k views

The problem

Question presented to candidate: "Design the home news feed for a social network like Facebook or LinkedIn. People follow friends, pages and groups, and when they open the app they should see the most relevant posts first, not just the newest. Assume a few hundred million daily users."

What a strong answer should cover:

  • Requirements and a capacity estimate: feed requests per second at peak, candidates per request, and why scoring every candidate with a heavy model is unaffordable
  • The pipeline: candidate generation (fan-out or pull from the people and pages you follow), multi-stage ranking (a cheap filter then a heavier model), then filtering (privacy, blocks, already seen) and hydration of post ids into full posts
  • Why a ranked feed cannot use offset pagination or a plain score cursor, and how a per-session feed snapshot gives stable pages while new posts wait behind a "new posts" banner
  • Seen-state and impression logging, which feed both deduplication and the ranking model
  • Storage choices: feed caches hold ids not post bodies, posts live in a sharded store, counters are aggregated asynchronously; what breaks first at 10x

Clarifying questions expected:

  • Ranked or chronological? If ranked, is there an existing model, or do we design the ranking signals too?
  • What sources feed the feed: only people you follow, or also groups, pages, recommendations and ads?
  • How fresh must it be: should a friend post appear within seconds, or is a minute fine?

Code / implementation expected: Optional โ€” a scoring function and a pagination demo that shows duplicates with offsets and stable pages with a snapshot; the example does both and computes the capacity numbers.

  • news-feed

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

16 min read ยท 16 sections

Read the code

Ranked news feed: capacity, scoring candidates, and offset vs keyset vs snapshot pagination when scores change between pages
// News feed: capacity, ranking candidates, and why ranked feeds paginate from a snapshot.
const fmt = (n) => n.toLocaleString("en-US");

console.log("== 1. Capacity (assumptions: 300M DAU, 10 feed opens/day, peak = 3x avg, 500 candidates/request) ==");
const dau = 300_000_000, opens = 10, cands = 500;
const perDay = dau * opens;
const avg = Math.floor(perDay / 86400);
const peak = avg * 3;
console.log("feed requests/day   " + fmt(perDay));
console.log("avg requests/s      " + fmt(avg));
console.log("peak requests/s     " + fmt(peak));
console.log("candidates scored/s " + fmt(peak * cands) + " at peak (one heavy model on all of them is too costly)");
console.log("stage 2 scores/s    " + fmt(peak * 50) + " if a light stage 1 keeps the top 50");
console.log("feed id cache       " + fmt(dau * cands * 8 / 1e9) + " GB (500 post ids x 8 bytes per user)");

console.log("\n== 2. Ranking: score = affinity x type weight x 10000 / (age_min + 120) + likes ==");
const affinity = { mom: 90, ana: 60, bob: 30, brand: 10 };
const typeW = { photo: 3, video: 3, link: 1, text: 2 };
// [id, author, type, age in minutes at session start, likes]
const posts = [
  ["p1", "brand", "video", 5, 400], ["p2", "bob", "text", 10, 2], ["p3", "ana", "photo", 30, 40],
  ["p4", "mom", "photo", 240, 15], ["p5", "bob", "link", 45, 1], ["p6", "ana", "text", 60, 8],
  ["p7", "brand", "link", 90, 900], ["p8", "mom", "text", 600, 3], ["p9", "bob", "photo", 120, 12],
  ["p10", "ana", "video", 300, 25],
].map(([id, author, type, age, likes]) => ({ id, author, type, age, likes }));
const score = (p) => Math.floor(affinity[p.author] * typeW[p.type] * 10000 / (p.age + 120)) + p.likes;
for (const p of posts) p.score = score(p);
const byTime = [...posts].sort((a, b) => a.age - b.age || (a.id < b.id ? -1 : 1));
const byScore = [...posts].sort((a, b) => b.score - a.score || (a.id < b.id ? -1 : 1));
console.log("chronological: " + byTime.map((p) => p.id).join(" "));
console.log("ranked:        " + byScore.map((p) => p.id + "(" + p.score + ")").join(" "));

console.log("\n== 3. Page 2 is requested 30 min later: ages grow, p9 gains 1500 likes, 2 new posts (page size 4) ==");
const fresh = [{ id: "n1", author: "ana", type: "photo", age: 0, likes: 0 }, { id: "n2", author: "mom", type: "photo", age: 0, likes: 0 }];
const later = posts.map((p) => ({ ...p, age: p.age + 30, likes: p.likes + (p.id === "p9" ? 1500 : 0) })).concat(fresh);
for (const p of later) p.score = score(p);
const rankedLater = [...later].sort((a, b) => b.score - a.score || (a.id < b.id ? -1 : 1));
console.log("re-ranked now: " + rankedLater.map((p) => p.id + "(" + p.score + ")").join(" "));
const page1 = byScore.slice(0, 4).map((p) => p.id);
const report = (name, p2, unreachable) => {
  const dup = p2.filter((id) => page1.includes(id));
  console.log(name.padEnd(24) + "page2=[" + p2.join(" ") + "] duplicates=" + dup.length + " never shown=" + (unreachable.join(" ") || "none"));
};
console.log("page1 (all methods)     [" + page1.join(" ") + "]");
// offset pagination: re-run the ranking, skip the first 4
const offsetP2 = rankedLater.slice(4, 8).map((p) => p.id);
report("offset re-query", offsetP2, rankedLater.slice(0, 4).filter((p) => !page1.includes(p.id) && !p.id.startsWith("n")).map((p) => p.id));
// keyset cursor on (score, id) of the last item on page 1
const cur = byScore[3];
const below = (p) => p.score < cur.score || (p.score === cur.score && p.id > cur.id);
const keyP2 = rankedLater.filter(below).slice(0, 4).map((p) => p.id);
report("keyset cursor on score", keyP2, rankedLater.filter((p) => !below(p) && !page1.includes(p.id) && !p.id.startsWith("n")).map((p) => p.id));
// snapshot: rank once at session start, store the ordered ids, page by position
const snapshot = byScore.map((p) => p.id);
report("session snapshot", snapshot.slice(4, 8), []);
console.log("snapshot banner: " + fresh.length + " new posts (shown at top on refresh)");

console.log("\n== 4. Next session: seen posts filtered, new posts ranked in ==");
const seen = new Set(snapshot.slice(0, 8));
const next = rankedLater.filter((p) => !seen.has(p.id));
console.log("seen=" + seen.size + " next feed: " + next.map((p) => p.id + "(" + p.score + ")").join(" "));

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 61 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

How would you design a distributed cache?

Continue