Design Twitter
Design a microblogging service. Users publish short public posts (up to 280 characters, optionally with media), follow other accounts, and read a home timeline of posts from the accounts they follow. They can also search all recent posts by keyword or hashtag, and see a list of topics that are trending right now in their country.
The service has a few hundred million daily users and a very uneven audience: most accounts are followed by a handful of people while a few have over a hundred million followers. Big live events produce sudden bursts of posting, and people expect those posts to be searchable and to show up in trends within seconds.
In 45 minutes, cover posting and home timelines, then spend real time on near-real-time search and on how trends are computed.
Sketch on the HLD board, write your answer in the Design doc, and size it with the Estimator. Start the mock interview from the timer for a 45-minute run.
Design Path · Social & Messaging · problem 4 of 18
Ask before you design. Try answering each yourself, then compare.
- Are posts public, or do we need per-post privacy?
Typical answer
Assume public posts. Protected accounts exist but we can treat them as a filter at read time.
- What scale should we plan for?
Typical answer
About 250 million daily users, 500 million new posts a day, and around 5 billion home-timeline loads a day.
- Is the home timeline ranked or chronological?
Typical answer
Reverse chronological is the default for this interview; mention where ranking would plug in.
- How fresh do search results and trends need to be?
Typical answer
A new post should be searchable within about 10 seconds. Trends should reflect the last 5 to 15 minutes and refresh every minute.
- How far back does search go?
Typical answer
Recent posts (the last 7 days) must be fast. Searching the full archive can be slower and served by a separate tier.
- Are trends global or local?
Typical answer
Per country plus a global list, with obvious spam and blocked terms filtered out.
- Do we count likes and reposts?
Typical answer
Yes, counters are shown on each post and can be eventually consistent by a few seconds.
Requirements
Functional
- Publish a post of up to 280 characters with optional media; delete own posts.
- Follow and unfollow accounts; read a home timeline and any user's profile timeline.
- Search recent posts by keyword, hashtag or author, newest or most relevant first.
- Show trending topics per country and globally, refreshed every minute.
Non-functional
- Home timeline p99 under 200 ms; 99.95% availability for reads.
- Posts appear in followers' timelines within 5 s and in search within 10 s at p95.
- Sustain 6 K posts/s average and 5x bursts during live events; ~60 K timeline reads/s average.
- Search p99 under 500 ms over the last 7 days of posts.
Out of scope
- Direct messages, ads and notification delivery.
- Media transcoding (assume blob storage plus CDN).
- Training any ranking or spam model.
Back-of-the-envelope estimates
- Posts: 500 M/day ≈ 6,000/s; live events can spike to 5× (30,000/s) for minutes.
- Timeline reads: 5 B/day ≈ 58,000/s, about 175,000/s at peak, so reads outnumber posts about 10:1.
- Fan-out: with roughly 200 followers on average, 500 M posts become ~100 B timeline inserts a day (≈1.2 M/s). This is why the most-followed accounts are pulled at read time instead.
- Post storage: ~300 bytes of text and metadata per post ≈ 150 GB/day, about 275 TB over 5 years and 820 TB with 3 replicas. Media is separate and much larger.
- Search index: 7 days ≈ 3.5 B posts. At ~150 bytes of postings per post the recent index is around 500 GB, which fits in memory across a few dozen shards per replica.
- Trends: every post is parsed for hashtags and phrases: ~6,000 events/s into the counting pipeline, trivial for a stream processor.
Fan-out volume and search freshness drive the design; raw storage is large but straightforward.
API
POST /v1/posts {text, mediaIds[], replyTo?} -> 201 {postId}
DELETE /v1/posts/{postId} -> 204
GET /v1/timeline/home?cursor=&limit=50 -> {posts[], nextCursor}
GET /v1/users/{id}/posts?cursor= -> {posts[], nextCursor}
POST /v1/follows {targetUserId} -> 204
GET /v1/search?q=&sort=recent|top&cursor= -> {posts[], nextCursor}
GET /v1/trends?country=IN -> {trends:[{term, postCount}]}Post ids are 64-bit, roughly time-ordered ids generated without a central counter, so the cursor for timelines and recent search is simply 'posts older than this id'.
Data model
posts(post_id PK, author_id, text, media_refs, reply_to, created_at) in a wide-column store, partitioned by post_id; a second table user_posts(author_id, post_id DESC) serves profile pages and the pull path for big accounts.
follows is kept in both directions (followers_of(user) and following(user)) in a sharded key-value store so fan-out and filtering are single-partition reads.
Timeline cache: a Redis list or sorted set of the newest ~800 post ids per active user. It is rebuildable, so it is not replicated across regions.
Search index: an inverted index from term to a list of post ids, partitioned first by time (one segment per few hours) and then by post id hash within each time slice. Counters (likes, reposts) live in a separate counter service that batches increments.
Trends: per-country rolling counts in the stream processor's state, with the published top 30 per country written to a small cache.
High-level design
Posting. The client calls the API gateway, which routes to the Post Service. It validates the text, assigns a time-ordered id, writes the post to the Post Store and publishes a post created event to the Posts stream. The request returns as soon as both writes succeed. Three independent consumers read that stream. Fan-out Workers look up the author's followers in the Follow Graph and push the post id into each active follower's list in the Timeline Cache, skipping authors with more than about 1 M followers. The Indexer tokenises the text and appends it to the newest segment of the Search Index, so the post is searchable within seconds. The Trend Aggregator extracts hashtags and phrases and updates per-country windowed counts, publishing a ranked list to the Trends Cache every minute.
Reading. The Timeline Service reads the viewer's cached id list, merges in recent posts from the big accounts they follow, filters blocked or deleted items, and hydrates ids from the Post Store through a post cache. The Search Service sends the query to every shard of the relevant time slices, newest first, and stops once it has enough results. Trends are a cheap key lookup in the Trends Cache.
Open the HLD board and press Reference to see the architecture diagram.
Deep dives
Home timelines with uneven audiences
Pushing each post id into every follower's cached timeline makes reads a single lookup, which matters at 175 K reads per second. The cost is write amplification, and for an account with 100 M followers a single post would mean 100 M cache writes taking minutes. So the fan-out workers skip accounts above a follower threshold, and the Timeline Service pulls those accounts' latest posts at read time and merges them in. A viewer follows few such accounts, and their recent-post lists are tiny and permanently cached. Fan-out also skips users who have not been active for weeks; their timeline is rebuilt from the follow list on their next login.
Near-real-time search
Indexing must keep up with 30 K posts per second in bursts and expose new posts within 10 seconds. Partition the index by time: the current segment is an in-memory, append-only structure that indexers write to; once it reaches a few hours of data it is frozen, compacted and becomes read-only. Within each time slice, shard by post id hash so writes spread evenly. A query fans out to all shards of the newest slice first and walks back in time only if it needs more results, which matches how people use 'latest' search. Postings lists are stored newest first, so early termination is cheap. Older data lives in a separate, disk-based archive tier with slower but acceptable latency. Deletes are handled with a tombstone set checked at query time, and segments are rewritten later.
Computing trends
A stream processor keys events by (country, term) and keeps counts in 1-minute buckets, summed over a 15-minute sliding window. Raw volume is a poor signal, because everyday tags are always popular, so each term's current rate is compared with its normal rate for that time of day, for example a z-score style ratio with smoothing for rare terms. To bound memory over millions of distinct terms, keep exact counts only for terms that pass a cheap approximate filter such as a count-min sketch. Before publishing, drop blocked terms, collapse near-duplicates, and require the term to come from many distinct accounts so a bot network cannot manufacture a trend.
Viral posts and hot counters
A post going viral gets thousands of likes per second and millions of reads. Never update the like count on the post row synchronously. The counter service buffers increments in memory per post for a second, writes one aggregated delta, and spreads very hot counters over several sub-keys that are summed on read. Reads of the post itself are served from a replicated post cache plus a small in-process cache on the Timeline Service for the hottest ids.
Failure modes
- If the search indexer falls behind, posts still appear in timelines; search shows a short freshness gap that is monitored as consumer lag.
- If a Timeline Cache shard is lost, affected users get timelines rebuilt on read from the follow graph and recent-post lists while the shard warms up.
- If the trend aggregator restarts, it restores window state from its checkpoint; the published trends cache keeps serving the last good list.
- If the Posts stream is unavailable, the Post Service keeps accepting posts into the store and an outbox relay replays them to the stream later, so no consumer misses a post.
Core algorithm in code
import hashlib
class CountMinSketch:
"""Approximate counts for an unbounded set of hashtags in fixed memory."""
def __init__(self, width=2048, depth=4):
self.w, self.d = width, depth
self.rows = [[0] * width for _ in range(depth)]
def _cols(self, key):
for i in range(self.d):
h = hashlib.blake2b(f"{i}:{key}".encode(), digest_size=8).digest()
yield i, int.from_bytes(h, "big") % self.w
def add(self, key, n=1):
for i, c in self._cols(key):
self.rows[i][c] += n
def estimate(self, key):
return min(self.rows[i][c] for i, c in self._cols(key))
def trend_score(current, baseline):
"""How unusual is this window's count compared with the usual rate?
Smoothing (+10) stops tiny tags jumping from 1 to 5 from looking huge."""
return (current - baseline) / (baseline + 10) ** 0.5
# Baseline: average count per 5-minute window over the past week (kept per tag by the aggregator).
baseline = {"#monday": 900, "#music": 400, "#eclipse": 3, "#football": 250}
now = CountMinSketch()
stream = ["#monday"] * 950 + ["#music"] * 420 + ["#eclipse"] * 600 + ["#football"] * 700 + ["#rare"] * 4
for tag in stream:
now.add(tag)
ranked = sorted(baseline, key=lambda t: trend_score(now.estimate(t), baseline[t]), reverse=True)
for tag in ranked:
c = now.estimate(tag)
print(f"{tag:<10} window={c:>4} usual={baseline[tag]:>4} score={trend_score(c, baseline[tag]):6.1f}")
print("most frequent is not most trending: #monday is busy but normal")
Trade-offs
- Fan-out on write gives cheap timeline reads but costs massive write amplification, so the hybrid pulls posts from very large accounts at read time.
- Time-partitioned search makes recent queries and ingestion fast but costs extra work for queries that span months.
- Comparing against a baseline finds genuinely rising topics but costs per-term history and can lag a few minutes behind a sudden spike.
- Approximate counting (sketches) bounds memory but costs small over-counts, so exact counts are kept only for candidate trends.
- Eventually consistent counters scale to viral posts but can show slightly different numbers to different viewers for a few seconds.
What a strong answer shows
- Uses numbers to justify a push/pull hybrid for timelines and handles very large accounts.
- Publishes one event per post and drives timelines, search and trends from it asynchronously.
- Designs a search index that is time-partitioned, appendable and queried newest first.
- Defines trending as unusual growth relative to a baseline, not raw volume, and considers spam.
- Keeps counters and hot posts from overloading a single row or shard.
- Discusses what degrades, and how, when each async consumer falls behind.
Mistakes to avoid
- Designing timelines only and leaving search and trends as a box labelled 'Elasticsearch'.
- Defining trends as the most frequent hashtags, which always returns the same everyday tags.
- Updating search, timelines and counters synchronously inside the post request.
- Sharding the search index by term, which creates enormous hot shards for popular terms during events.
Follow-ups to expect
- How do you handle edits or deletes in search? (Tombstone ids at query time, rewrite segments during compaction.)
- How would you personalise trends? (Blend local trends with topics from accounts the user follows, computed as a lightweight re-rank.)
- How would you support 'top' search instead of 'latest'? (Collect candidates newest first, then re-rank by engagement and author quality.)
- How would you keep a bot network from creating a fake trend? (Count distinct, aged, non-flagged accounts rather than raw posts.)