Skip to the solution
HardSystem Design14 min read

How would you design Twitter's timeline at scale?

767 views

The problem

Question presented to candidate: "Design the home timeline for Twitter, the list of recent tweets from everyone you follow. Reads vastly outnumber writes, some accounts have tens of millions of followers, and a tweet should show up for followers within a few seconds."

What a strong answer should cover:

  • The read/write asymmetry (Twitter reported roughly 300K timeline reads per second against about 6,000 tweet writes per second in 2012) and why that favours precomputing timelines
  • Fan-out on write (push tweet ids into every follower timeline) vs fan-out on read (merge followees at read time), with the cost of each counted, not just named
  • The celebrity problem and the hybrid: push for normal accounts, pull high-follower accounts at read time and merge, with a threshold chosen from numbers
  • Timeline storage: capped in-memory lists of tweet ids, hydration from the tweet store, rebuilding a timeline for an inactive user on demand
  • Edge cases: deletes, unfollows, protected accounts, timeline cap and scrolling past it; what breaks first at 10x

Clarifying questions expected:

  • Chronological only, or ranked? (This answer is chronological; ranking is the news feed question.)
  • How many users, follows per user, tweets and timeline reads per day? What is the largest follower count?
  • How quickly must a tweet appear for followers, and how far back can a user scroll?

Code / implementation expected: Optional โ€” counting writes and reads for push, pull and hybrid on a skewed follower graph, and showing all three produce the same timeline; the example does both.

  • twitter
  • timeline

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

14 min read ยท 16 sections

Read the code

Home timeline fan-out: push vs pull vs hybrid on a skewed follower graph, and the same timeline from all three via a k-way merge
// Twitter-style home timelines: push vs pull vs hybrid fan-out, counted on a skewed follower graph.
const fmt = (n) => n.toLocaleString("en-US");
const USERS = 1_000_000, TWEETS_PER_USER = 2, READS_PER_USER = 10;
// skewed tiers: [accounts, followers each]
const tiers = [[10, 500_000], [990, 50_000], [99_000, 1_000], [900_000, 100]];
const followers = [];
for (const [n, f] of tiers) for (let i = 0; i < n; i++) followers.push(f);
const edges = followers.reduce((a, b) => a + b, 0);
console.log("== 1. Graph: " + fmt(USERS) + " users, " + fmt(edges) + " follow edges, avg " + Math.floor(edges / USERS) + " followees per user ==");
console.log("top account followers " + fmt(followers[0]) + ", median account followers " + fmt(followers[USERS / 2]));
const tweets = USERS * TWEETS_PER_USER, reads = USERS * READS_PER_USER;
console.log("per day: " + fmt(tweets) + " tweets, " + fmt(reads) + " timeline reads\n");

console.log("strategy          timeline writes/day  fetches/read     fetches/day  largest fan-out");
const row = (name, writes, perRead, worst) =>
  console.log(name.padEnd(18) + fmt(writes).padStart(19) + String(perRead).padStart(14) + fmt(perRead * reads).padStart(16) + fmt(worst).padStart(17));
row("push (on write)", TWEETS_PER_USER * edges, 1, followers[0]);
row("pull (on read)", 0, Math.floor(edges / USERS), 0);
for (const T of [100_000, 10_000]) {
  let pushedEdges = 0, celebEdges = 0, celebs = 0, worst = 0;
  for (const f of followers) { if (f > T) { celebEdges += f; celebs++; } else { pushedEdges += f; worst = Math.max(worst, f); } }
  // a read = 1 precomputed list + one fetch per followed celebrity (average)
  const perRead = 1 + Math.ceil(celebEdges / USERS);
  row("hybrid T=" + fmt(T), TWEETS_PER_USER * pushedEdges, perRead, worst);
  console.log("                  (" + fmt(celebs) + " accounts above T are pulled at read time)");
}
const rate = 10_000;
console.log("\npush: one tweet by the top account at " + fmt(rate) + " inserts/s (one fan-out worker) takes " + followers[0] / rate + " s to reach every follower");

console.log("\n== 2. Same timeline from all three strategies (cap = 5 ids per cached timeline) ==");
const CAP = 5;
const follows = { alice: ["bob", "carol", "star"] };
const isCeleb = { star: true };
// [time, author, id]; ids are what the timeline stores, tweet bodies are hydrated later
const posted = [[1, "bob", "b1"], [2, "star", "s1"], [3, "carol", "c1"], [4, "bob", "b2"], [5, "star", "s2"],
  [6, "carol", "c2"], [7, "bob", "b3"], [8, "carol", "c3"], [9, "star", "s3"], [10, "bob", "b4"]];
const userTweets = {}, home = { alice: [] };
for (const [t, author, id] of posted) {
  (userTweets[author] ||= []).unshift({ t, id }); // each author keeps their own list, newest first
}
const mergeLists = (lists, limit) => { // k-way merge of newest-first lists
  const idx = lists.map(() => 0), out = [];
  while (out.length < limit) {
    let best = -1;
    for (let i = 0; i < lists.length; i++)
      if (idx[i] < lists[i].length && (best < 0 || lists[i][idx[i]].t > lists[best][idx[best]].t)) best = i;
    if (best < 0) break;
    out.push(lists[best][idx[best]++]);
  }
  return out;
};
const show = (name, list) => console.log(name.padEnd(8) + list.map((x) => x.id).join(" "));
// pull: merge every followee list at read time
show("pull", mergeLists(follows.alice.map((a) => userTweets[a]), CAP));
// push: each tweet is written into every follower list at post time, list trimmed to CAP
let writes = 0;
for (const [t, author, id] of posted) if (follows.alice.includes(author)) { home.alice.unshift({ t, id }); writes++; }
home.alice = home.alice.slice(0, CAP);
show("push", home.alice);
// hybrid: only non-celebrities are pushed; celebrities are merged in at read time
let hybridList = [], hybridWrites = 0;
for (const [t, author, id] of posted) if (follows.alice.includes(author) && !isCeleb[author]) { hybridList.unshift({ t, id }); hybridWrites++; }
hybridList = hybridList.slice(0, CAP);
show("hybrid", mergeLists([hybridList, ...follows.alice.filter((a) => isCeleb[a]).map((a) => userTweets[a])], CAP));
console.log("writes into alice timeline: push=" + writes + " hybrid=" + hybridWrites + "; reads fetch 1 list (push), 2 lists (hybrid), 3 lists (pull)");
const older = mergeLists(follows.alice.map((a) => userTweets[a].filter((x) => x.t < home.alice[CAP - 1].t)), CAP);
console.log("scroll past the cap: fall back to pull for tweets older than t=" + home.alice[CAP - 1].t + ": " + older.map((x) => x.id).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 71 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 Mint.com (personal finance aggregator)?

Continue