Blueprint
Social & feeds intermediate

Design Twitter/X

The feed question with the training wheels off: the fan-out core is assumed knowledge, and the marks are in IDs across shards, search over a firehose, and counters that survive going viral.

asked at Meta · Amazon · X Frequently asked ~40 min walkthrough
01

Scope the requirements

This overlaps the news feed deliberately — interviewers expect the fan-out core stated quickly and confidently, and then they push on what Twitter adds: time-ordered IDs without coordination, search over immutable tweets, trending topics, and like counts on a tweet the whole planet is tapping.

Functional — what it must do

  • Post tweets — 280 characters, optional media.
  • Follow users; a home timeline of followees' tweets, reverse-chronological.
  • Like and retweet, with visible counts.
  • Search tweets by keyword; trending topics as a stretch goal I'll plan for.
  • A user timeline (one profile's own tweets) — distinct from the home timeline, and I say so early.
  • Explicitly cut: DMs, notifications, ads, ML ranking.

Non-functional — the numbers that shape the design

  • Home timeline p99 under 500 ms; a tweet is visible to followers within ~5 seconds — Twitter's real production target.
  • Availability over consistency — eventual is fine everywhere, and like counts may be approximate.
  • Brutally read-skewed: real Twitter runs roughly 300k timeline reads/s against ~6k tweet writes/s — a 50:1 skew the architecture must reflect.
  • Scale target: 200M DAU, 100M tweets/day.
  • Tweets are immutable once posted — an assumption I'll exploit for search and caching.
Say this out loud

I'll state the constraint that shapes everything up front: this system is 50 parts read to one part write, and tweets never change after creation. Every choice — precomputed timelines, index-once search, aggressive caching — falls out of those two facts.

02

Back-of-envelope estimates

The Grokking-canonical numbers, and worth having cold — the read/write skew justifies the architecture, and the media line shows where the bytes actually are.

QuantityEstimateHow you got there
Tweet writes~1k/s100M tweets/day ÷ ~86k s ≈ 1,150/s; peaks 5–10×.
Timeline reads~300k/s200M DAU visiting timelines ≈ 28B tweet views/day. The 50:1 skew, visible in the arithmetic.
Text storage~30 GB/day100M × ~300 B ≈ 30 GB/day, ~11 TB/yr. Tweet text is a rounding error.
Media storage~24 TB/day1 in 5 tweets a photo (200 KB) + 1 in 10 a video (2 MB). Media dwarfs text 800:1 — it goes to S3 + CDN, never the database.
Fan-out volume~20B inserts/day100M tweets × ~200 avg followers ≈ 200k timeline-cache writes/s, absorbed async.
Timeline cache entry~800 IDs/userReal Twitter keeps a Redis list of ~800 tweet IDs per active user's home timeline.
Say this out loud

Three hundred thousand reads a second with a 500 ms budget means the home timeline must be a precomputed list in RAM — Redis lists of about 800 tweet IDs per active user, which is exactly what Twitter actually runs.

03

Sketch the API

Five endpoints. The quiet decision that pays off later: the pagination cursor is the tweet ID itself, which only works because of how I mint IDs.

POST/api/v1/tweetsbody: { text, mediaIds[]? } → { tweetId }. Media uploaded separately via presigned URLs; this call is metadata only.
GET/api/v1/timeline/home?cursor={tweetId}The hot path — precomputed IDs from Redis, hydrated, celebrity tweets merged in.
GET/api/v1/users/{id}/tweets?cursor=...User timeline — a direct (authorId, time) index query. No fan-out machinery.
POST/api/v1/tweets/{id}/likeIdempotent per (user, tweet); same shape for retweet. Hits counters, not the tweet row.
GET/api/v1/search?q=...&cursor=...Served by the search cluster, never by the tweet store.
  • The cursor is a Snowflake ID: because IDs embed their creation time, "tweets older than cursor" is a pure ID comparison — no timestamp column, no offset, stable under the firehose of inserts.
  • Like/retweet are idempotent per (userId, tweetId) — a unique key on the engagement row means double-taps and retries can't double-count.
04

Data model

Tweets are immutable and append-only, engagement is counters, timelines are cache. Three very different write patterns, so they get three different stores.

tweets
tweetId PK (Snowflake) · authorId · text · mediaUrl? · createdAt
Immutable. Partitioned by tweetId; index on (authorId, time) for user timelines and celebrity pulls.
follows
(followerId, followeeId) composite PK
Indexed both directions — fan-out needs "who follows me", the UI needs "who do I follow".
engagement
tweetId → like/retweet counts (+ per-user rows for idempotency)
Counters live in Redis, flushed to the store periodically — never a row lock on the hot tweet.
home_timeline
userId → Redis list of ~800 tweet IDs
Cache, not truth. Only for active users; rebuilt by pull on login for dormant ones.
The database call

Tweets go to Cassandra or DynamoDB — 6k immutable writes/s with point reads by ID is the textbook partitioned-KV workload, and there are no joins to lose. It's worth saying that real Twitter historically ran sharded MySQL and it worked; at 10M DAU I'd do the same. I'd switch to the partitioned store when operating the shards myself stops being fun — around this scale.

05

High-level design

One write enters, and Kafka fans it to three consumers with different jobs: timeline fan-out, the search indexer, and the trends pipeline. The read side never sees any of that machinery — it reads finished results out of caches and indexes.

client edge service data async Client Load balancer Tweet service Timeline service Search service Tweet store Cassandra Timeline cache Redis: 800 IDs/user Elasticsearch inverted index Firehose Kafka Fan-out workers + trends counting insert fan-in query 800 IDs GET /home push IDs POST /tweets tweet event hydrate, celeb GET /search index once
one tweet write feeds three async consumers off kafka — timelines, search, trends. notice the read services only ever touch finished data structures, never the pipeline.
  1. Tweet: client POSTs → tweet service mints a Snowflake ID, inserts into Cassandra, drops the tweet on the Kafka firehose, returns. Sub-100 ms for the author.
  2. Fan-out workers push the ID into each follower's Redis timeline list (trim at ~800), skipping flagged celebrity accounts — the ~5 s visibility target is the queue's SLO.
  3. In parallel, the indexer consumes the same event and writes the tweet into Elasticsearch exactly once — immutability means no update path — while the trends pipeline bumps sliding-window term counts.
  4. Home timeline read: timeline service fetches the ID list from Redis, hydrates via a tweet cache backed by Cassandra, merges recent tweets from followed celebrities pulled live, attaches counts from the counter cache.
  5. Search read: query fans in across all Elasticsearch shards, results merge and rank — the mirror image of the timeline's fan-out.
06

Deep dives — where the interview is won

Phase 01

Snowflake IDs: time-ordered, collision-free, no coordinator

Auto-increment dies the moment tweets live on more than one database node — either the nodes collide or they serialise on a shared sequence, and a central ticket server becomes a single point of failure at 6k writes/s. Hashing gives uniqueness but random IDs, which destroys the thing a timeline needs most: chronological order you can sort and paginate by. Snowflake solves both in 64 bits: 41 bits of millisecond timestamp, 10 bits of machine ID, 12 bits of per-machine sequence — 4,096 IDs per millisecond per machine, minted entirely locally.

The timestamp in the high bits is the trick: sorting by ID is sorting by time, so cursor pagination, timeline merges, and even time-range scans on the partition key all fall out for free, with zero coordination between machines. The operational fine print is clock skew — machines sync via NTP, and a generator that sees its clock jump backwards must refuse to mint until time catches up, or it risks duplicate IDs.

machines id Node 12 Node 47 Node 83 64-bit id ts | node | seq
each server has a machine id. timestamp + machine + seq gives sortable, coordination-free 64-bit ids.
Trade-offOrdering is only k-sorted — two tweets in the same millisecond on different machines have arbitrary relative order. For a feed that's invisible; for a ledger it would be disqualifying, and saying where the guarantee breaks is worth marks.
Phase 02

Hybrid fan-out, with Twitter's actual numbers attached

The strategy is the news-feed answer — push for normal accounts, pull-and-merge for celebrities — so I state it fast and spend my time on what production taught Twitter. Home timelines live in Redis as lists of ~800 tweet IDs; fan-out runs at roughly 20B inserts/day (~200k/s), and delivery within ~5 seconds is the pipeline's SLO. Accounts above a follower threshold skip fan-out and are merged at read time. That's the whole core, and it's exactly what Twitter shipped.

The number interviewers push on is RAM. Naively, 800 IDs × 8 B × every registered user is memory you don't want to buy — so only active users hold a timeline in Redis. A dormant user's list is simply absent; on login, the timeline service rebuilds it once by pulling followees' recent tweets, then fan-out resumes for them. Most registered users are dormant at any moment, so this halves the cache several times over.

author path viewer Normal user Celebrity Fanout writer Celeb store Merge on read write→N just write 1
normal users fan out on write. celebrities fan out on read. merged at request time.
Trade-offA returning user's first load is a slow pull-and-rebuild rather than a cache hit — one degraded request per user per absence, in exchange for keeping the Redis fleet sized to actives instead of everyone who ever signed up.
Phase 03

Search and trends ride the firehose, not the database

Search over tweets cannot be a LIKE '%term%' scan — that's a full table walk per query. It needs an inverted index: term → list of tweet IDs, which is Elasticsearch's whole job. The gift is immutability: a tweet is indexed exactly once, by an async consumer on the Kafka firehose, and never updated. No reindex-on-edit path, no cache invalidation on documents — seconds of indexing lag is the entire consistency story. Note the symmetry worth saying aloud: timelines fan out on write so reads are cheap; search fans in on read — every query hits all index shards and merges — because you can't know at write time which queries will want a tweet.

Trends are a streaming problem, not a query: find heavy-hitter terms over a sliding window of the firehose. Exact counting of every term is pointless — a count-min sketch plus a top-k heap per window gives approximate counts in fixed memory, and nobody audits whether a hashtag had 1.02M or 1.05M mentions. Windowed counts land in a small trends cache that the read path serves in one hit.

write bus index Write path Tweet firehose Search index Trends job publish
write path emits to kafka; search + trends are just consumers of the same firehose.
Trade-offBoth systems are deliberately behind: search lags seconds, trends are approximate by construction. I'm trading freshness and exactness for a read path that never touches the write path — the right trade everywhere on this product except payments-grade counts, which it doesn't have.
Phase 04

Counters that survive a viral tweet

A tweet going viral means millions of likes an hour landing on one key — as a database row that's a lock convoy, as a single Redis key it's better but still one node's ceiling. The base pattern: likes hit Redis INCR, a consumer flushes deltas to durable storage periodically, and the per-user engagement row (for idempotency and un-like) is written async. Reads come from the cache, display values round to "1.2M", and nobody waits on durable writes.

For the truly extreme key, shard the counter: split it into N sub-keys (likes:{id}:0..15), increment one at random, sum on read — write throughput scales by N for the price of a slightly heavier read, which is itself cached. This is the general hot-key recipe: spread writes, aggregate lazily.

click counter db Like Redis INCR Batched writer counters +1 flush
hot counter goes to redis; async writer batches to db. read is always redis.
Trade-offCounts are eventually consistent and briefly wrong — a like may not show for seconds, and a crash can lose a small delta window. Exact real-time counts would mean serialising the planet on one row; approximate is the product-correct answer and I'd defend it as such.
07

Follow-ups you should expect

What's the difference between the home timeline and the user timeline in your design?
Completely different machinery. The user timeline is a direct index query — tweets by (authorId, time) — cheap, always fresh, no cache required beyond normal hydration. The home timeline is the precomputed Redis list assembled by fan-out. Conflating them is the fastest way to fail this question, because the celebrity pull path is literally "serve their user timeline into your home timeline".
A tweet gets deleted after being retweeted and fanned out into millions of timelines. How do you remove it?
I don't chase it — I write a tombstone. The tweet row is marked deleted; hydration filters tombstoned IDs at read time, so it vanishes from every timeline immediately without touching a single Redis list. Retweets hydrate through the same row, so they die with it. A lazy background pass trims dead IDs from caches eventually.
How does this go multi-region?
Reads localise easily — timeline caches, tweet caches and search clusters replicate per region, serving from local RAM. Writes are the hard part: I'd start with a single write region plus async replication, accepting a second or two of cross-region staleness, which the 5 s visibility SLO absorbs. Geo-partitioning users by home region is the later optimisation, and it complicates fan-out across region boundaries — I'd name that cost before volunteering it.
Why not just use database auto-increment for tweet IDs?
It doesn't survive sharding: either every shard hands out colliding IDs or they all queue on one sequence, which becomes a coordination bottleneck and a single point of failure. Snowflake gives uniqueness and time-ordering with no coordination at all, and the time-ordering is load-bearing — my pagination cursor and timeline merges depend on it.
Your Redis timeline cluster loses a node. What does the user see?
Timelines on that node are rebuilt on demand by the pull path — followees' recent tweets merged live — so users see a slower load, not an error. It's the same code path dormant users already exercise on login, which is the nice property: my failure mode is a pre-existing, well-tested path, not a special case.

Where candidates lose the room

  • Conflating the home timeline with the user timeline — they have opposite cost profiles, and the celebrity fix depends on knowing the difference.
  • Rebuilding the entire news-feed fan-out discussion from scratch and running out of time before search, trends and counters — the parts this question is actually about.
  • Proposing auto-increment or UUIDv4 for tweet IDs — one doesn't shard, the other doesn't sort; the Snowflake discussion is where the marks are.
  • Treating search as a SQL LIKE query instead of an async-built inverted index — it signals you've never thought about read amplification.
  • Designing as if reads and writes were comparable when the skew is 50:1 — every architectural choice should visibly favour the read path.

Concepts this leans on

Found a bug or want more?

Tell me what's broken, missing, or wrong. Every message lands in my inbox.