355. Design Twitter

Design a simplified Twitter where users post tweets and follow each other. getNewsFeedmust return the 10 most-recent tweets from a user's followees (including themselves) in O(k log k) time using a k-way merge via a max-heap over per-user tweet cursors.

MediumK-Way MergeDesign / OOPHeap / Priority QueueTypeScript

PROBLEM What we're solving

Implement four methods on a Twitter class: postTweet(userId, tweetId), getNewsFeed(userId) → the 10 newest tweet IDs from people the user follows (including themselves), follow, and unfollow.

Concrete example.User 1 posts tweets 5, 3 (in that order). User 2 posts tweet 10. User 1 follows user 2. getNewsFeed(1) [10, 3, 5]— tweet 10 is newest (latest global timestamp), then user 1's own tweets newest-first. After unfollow(1, 2), getNewsFeed(1) [3, 5].

KEY IDEA Global timestamp + k-way merge

Insight →stamp every tweet with a globally incrementing counter so you have a total order. Each user's tweet list is then sorted ascending by that counter. getNewsFeed is a k-way merge— maintain one "cursor" per followee pointing at their most recent un-consumed tweet, then greedily pull the globally newest tweet (highest timestamp) up to 10 times. That is exactly what a max-heap does.

RECIPE Global clock · per-user log · k-way merge

  • 0 · Global clock. Keep a static counter ts that increments with every postTweet. This gives tweets a comparable total order across users without any real wall clock.
  • 1 · Per-user log.Store each user's tweets as an append-only array [{ tweetId, time }, …]. Because you always push, the array is already sorted ascending — newest tweet is at the tail.
  • 2 · Seed cursors. In getNewsFeed, collect the set of peers (self ∪ followees). For each peer who has tweets, create a cursor pointing at their last (newest) tweet.
  • 3 · K-way merge. Repeat up to 10 times: find the cursor with the maximum time(that's the globally newest unconsumed tweet), emit its tweetId, advance that cursor one step left. Remove exhausted cursors. A real heap does this in O(log k) per pick; the linear scan shown here is O(k) per pick but identical in logic.
  • 4 · Follow / unfollow. A Map<number, Set<number>> gives O(1) add/delete. Nothing else needs to change — the merge reads the set fresh every call.
Classic confusion → many implementations forget to include the user's owntweets in the feed. The spec says "self + followees", so seed a cursor for userIdtoo, even if they don't appear in their own following set.

COST Complexity & alternatives

Collect all tweets, sort
O(T log T)
T = total tweets ever posted. Dominates for large histories.
K-way merge (heap)
O(k log k)
k = distinct followees; each of the 10 pops costs O(log k).

Space: O(T) for all tweet logs (unavoidable) + O(F) for the follow-graph where F = total follow edges.

Real heap vs linear scan: with a true priority queue (e.g. MinHeapkeyed by negative timestamp) each pick is O(log k). With a linear scan over k cursors each pick is O(k). Both give the same result; the heap wins when k is large (a celebrity followed by millions).

Pattern transfer → k-way merge appears in Merge k Sorted Lists (classic heap), Find K Pairs with Smallest Sums, and Kth Smallest Element in a Sorted Matrix. In all of them the heap holds one "cursor" per source list and you pop the min (or max) greedily.

RUN IT Per-user logs + k-way merge for the news feed

step 0 / 14
STARTTwitter object created. Global timestamp ts = 0.
1class Twitter {
2 private static ts = 0; // global monotonic timestamp
3 private tweets = new Map<number, { tweetId: number; time: number }[]>();
4 private following = new Map<number, Set<number>>();
5
6 postTweet(userId: number, tweetId: number): void {
7 if (!this.tweets.has(userId)) this.tweets.set(userId, []);
8 this.tweets.get(userId)!.push({ tweetId, time: Twitter.ts++ });
9 }
10
11 getNewsFeed(userId: number): number[] {
12 // Collect latest tweet from every followee (plus self)
13 const peers = new Set([userId, ...(this.following.get(userId) ?? [])]);
14 // Min-heap keyed by time (max-10 merge via a max-heap simulation)
15 // We use a simple sorted merge of per-user pointers for clarity.
16 type Cursor = { list: { tweetId: number; time: number }[]; idx: number };
17 const cursors: Cursor[] = [];
18 for (const uid of peers) {
19 const list = this.tweets.get(uid) ?? [];
20 if (list.length > 0) cursors.push({ list, idx: list.length - 1 });
21 }
22
23 const result: number[] = [];
24 while (result.length < 10 && cursors.length > 0) {
25 // pick the cursor with the largest time (max-heap by time)
26 let best = 0;
27 for (let i = 1; i < cursors.length; i++) {
28 if (cursors[i].list[cursors[i].idx].time >
29 cursors[best].list[cursors[best].idx].time) {
30 best = i;
31 }
32 }
33 const cur = cursors[best];
34 result.push(cur.list[cur.idx].tweetId);
35 if (cur.idx === 0) {
36 cursors.splice(best, 1); // exhausted this user's list
37 } else {
38 cur.idx--;
39 }
40 }
41 return result;
42 }
43
44 follow(followerId: number, followeeId: number): void {
45 if (!this.following.has(followerId)) this.following.set(followerId, new Set());
46 this.following.get(followerId)!.add(followeeId);
47 }
48
49 unfollow(followerId: number, followeeId: number): void {
50 this.following.get(followerId)?.delete(followeeId);
51 }
52}
State
0
ts
{}
following
peers / ts counteractive cursor / new logpicked tweet / resultunfollowfollowing map
slowfast

TYPESCRIPT The solution, annotated

designTwitter.ts
class Twitter {
  private static ts = 0;                      // global monotonic timestamp
  private tweets = new Map<number, { tweetId: number; time: number }[]>();
  private following = new Map<number, Set<number>>();

  postTweet(userId: number, tweetId: number): void {
    if (!this.tweets.has(userId)) this.tweets.set(userId, []);
    this.tweets.get(userId)!.push({ tweetId, time: Twitter.ts++ });
  }

  getNewsFeed(userId: number): number[] {
    // Collect latest tweet from every followee (plus self)
    const peers = new Set([userId, ...(this.following.get(userId) ?? [])]);
    // Min-heap keyed by time (max-10 merge via a max-heap simulation)
    // We use a simple sorted merge of per-user pointers for clarity.
    type Cursor = { list: { tweetId: number; time: number }[]; idx: number };
    const cursors: Cursor[] = [];
    for (const uid of peers) {
      const list = this.tweets.get(uid) ?? [];
      if (list.length > 0) cursors.push({ list, idx: list.length - 1 });
    }

    const result: number[] = [];
    while (result.length < 10 && cursors.length > 0) {
      // pick the cursor with the largest time (max-heap by time)
      let best = 0;
      for (let i = 1; i < cursors.length; i++) {
        if (cursors[i].list[cursors[i].idx].time >
            cursors[best].list[cursors[best].idx].time) {
          best = i;
        }
      }
      const cur = cursors[best];
      result.push(cur.list[cur.idx].tweetId);
      if (cur.idx === 0) {
        cursors.splice(best, 1);   // exhausted this user's list
      } else {
        cur.idx--;
      }
    }
    return result;
  }

  follow(followerId: number, followeeId: number): void {
    if (!this.following.has(followerId)) this.following.set(followerId, new Set());
    this.following.get(followerId)!.add(followeeId);
  }

  unfollow(followerId: number, followeeId: number): void {
    this.following.get(followerId)?.delete(followeeId);
  }
}

Reading it block by block

Lines 2–3 — storage. tweets maps each user to their append-only tweet log (sorted ascending by time because we always push). following maps each user to the set of people they follow. A Set gives O(1) follow/unfollow and deduplicates.
Lines 6–9 — postTweet.Assign the current global timestamp (then increment it) so every tweet has a unique, comparable age. Push to the user's list — the list stays sorted ascending, so the newest tweet is always at the tail.
Lines 11–22 — getNewsFeed setup. Build the peer set (self ∪ followees). For each peer with at least one tweet, create a cursor { list, idx: list.length - 1 }pointing at their newest tweet. This is the "seed" step of k-way merge — one entry per source.
Lines 24–40 — k-way merge loop. While we have fewer than 10 results and there are still cursors: find the cursor with the max time (linear scan here; replace with a heap for O(log k)). Emit its tweetId, advance its index. If the cursor hits index −1, splice it out (that user's tweets are exhausted).
Lines 43–50 — follow / unfollow. Just Set.add and Set.delete. No need to rebuild any index — getNewsFeed reads the set fresh every time it is called.
Complexity → postTweet: O(1). follow/unfollow: O(1). getNewsFeed: O(k · 10) with a linear scan = O(k), or O(10 log k) with a real max-heap — where k = number of followees. The result is always at most 10 items.

INTERVIEWFollow-ups they'll ask

  • "Replace the linear scan with a real heap." Push all k seed cursors into a max-heap keyed by time; each of the 10 pops is O(log k). Total: O(k + 10 log k).
  • "What if a user has millions of tweets?"Cap each user's log to the last N tweets (e.g. 1 000) — you only ever need the top-10 anyway, and you can prove N ≥ 10 is enough if the user has at most N-followees.
  • "Make getNewsFeed O(1) amortised." Materialise the feed on each post/follow/unfollow (write-heavy but read-cheap), or use a sorted merged timeline with lazy invalidation.
  • "Handle concurrent postTweet calls." The static counter needs to be an atomic integer (or use a mutex) in a multi-threaded environment.
  • "Return actual Tweet objects, not just IDs." Store a separate Map<number, Tweet> keyed by tweetId and look up after the merge.

MNEMONIC The one-liner

"Clock every tweet, cursor the tail, greedily pop the freshest — that's a news feed."

TRIGGERS When you see ___ → reach for ___

"merge N sorted streams / lists"k-way merge with max-heap
"most recent across multiple users"global timestamp + cursors
"design a social feed"per-user tweet log + follow set
"top-10 from multiple sorted arrays"heap with one cursor per array

SKELETON The reusable shape

skeleton.ts
class Twitter {
  private static ts = 0;
  private tweets = new Map<number, { tweetId: number; time: number }[]>();
  private following = new Map<number, Set<number>>();

  postTweet(userId: number, tweetId: number): void {
    // push { tweetId, time: Twitter.ts++ } to tweets[userId]
  }

  getNewsFeed(userId: number): number[] {
    // merge cursors (self + followees), pick max-time 10 times
    const result: number[] = [];
    // while result.length < 10 && cursors.length > 0 → find best, advance
    return result;
  }

  follow(followerId: number, followeeId: number): void { /* add to Set */ }
  unfollow(followerId: number, followeeId: number): void { /* delete from Set */ }
}

FLASHCARDS Tap to flip

Why stamp tweets with a global counter instead of a Date?
A monotonic integer gives a simple, deterministic total order. Dates can collide and are harder to compare.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 9
QUESTION 01
User 1 posts tweet 5 (ts=0), then tweet 3 (ts=1). User 2 posts tweet 10 (ts=2). User 1 follows user 2. getNewsFeed(1) returns:
QUESTION 02
What is the purpose of the global monotonic timestamp?
QUESTION 03
Time complexity of getNewsFeed when using a real max-heap (k = followees)?
QUESTION 04
After unfollow(1, 2), why does getNewsFeed(1) no longer include user 2's tweets?
QUESTION 05
What is the 'classic confusion' bug in this problem?
QUESTION 06
Why is each per-user tweet list already sorted by time without extra effort?
QUESTION 07
A user follows 500 people. Compared to "collect all their tweets and sort", the heap approach is faster because:
QUESTION 08
#355 · Design TwitterEach user has a per-user tweet log with a global timestamp counter. getNewsFeed merges the latest tweets from all followees via a max-heap k-way merge, taking the 10 most recent.Which algorithmic approach does this primarily use?
QUESTION 09
#355 · Design TwitterEach user has a per-user tweet log with a global timestamp counter. getNewsFeed merges the latest tweets from all followees via a max-heap k-way merge, taking the 10 most recent.Which implementation correctly solves it?