System Design Case Study · High-Level Design

Designing Twitter

A read-heavy social feed at planetary scale: how tweets are written once, fanned out to millions of home timelines, and served back in under 200 ms — with the fan-out hybrid, sharding scheme, and caching layers that make it work.

200 Mdaily active users
~5,800/stweet writes (avg)
~1.2 M/stimeline reads (peak)
200 : 1read / write ratio
< 200 msp99 timeline load

01Requirements & scope

Pin the problem before drawing boxes. Interviews reward a crisp scope statement more than an exhaustive feature list.

✅ Functional — in scope

  • Post a tweet (280 chars, optional image/video)
  • Follow / unfollow users
  • Home timeline — tweets from people you follow, reverse-chronological (ranking noted as an extension)
  • User timeline — one user's own tweets
  • Like, retweet, reply (counts on each tweet)
  • Search tweets; trending topics

🚫 Out of scope

  • DMs, Spaces, communities, ads serving
  • ML ranking model internals (we design the hook, not the model)
  • Moderation / spam pipelines
  • Analytics & experimentation platform

Non-functional requirements

PropertyTargetWhy it shapes the design
Latencyp99 < 200 ms timeline · < 500 ms tweet postForces precomputed timelines + aggressive caching; nothing on the read path may touch a cold database.
Availability99.99 % readsReads must degrade gracefully (stale timeline > error page). Favor AP over CP for the feed.
ConsistencyEventual (seconds)A follower seeing a tweet 5 s late is fine; losing an accepted tweet is not. Durability on write, eventual visibility.
Scale skewFollowers: median ~200, max 100 M+The celebrity long tail breaks naive fan-out — this single fact drives the core architectural decision.
The one-sentence framing: Twitter is a write-once, read-many system with extreme fan-out skew. Everything below is a consequence of optimizing the read path and taming the celebrity problem.

02Back-of-envelope estimation

Round numbers, stated assumptions. The goal is orders of magnitude that justify (or kill) design choices.

Traffic

QuantityWorkingResult
Daily active users (DAU)assumption200 M
Tweets written200 M × 2.5 tweets/day500 M/day ≈ 5,800/s avg · ~17 k/s peak
Timeline reads200 M × ~50 loads/day10 B/day ≈ 115 k/s avg · ~1.2 M/s peak (incl. polling & refresh)
Fan-out deliveries500 M tweets × ~200 followers100 B timeline inserts/day ≈ 1.2 M/s

Storage

QuantityWorkingResult
Tweet record (text + metadata)~1 KB × 500 M/day0.5 TB/day → ~1 PB / 5 yr (×3 replication: 3 PB)
Media20 % of tweets × 2 MB avg200 TB/day → object store + CDN, lifecycle-tiered
Timeline cache (Redis)200 M active users × 800 IDs × ~20 B~3.2 TB — fits a modest Redis cluster; cache active users only
What the math tells us: writes (5.8 k/s) are trivial; fan-out (1.2 M/s inserts) is the real write load, and it's amplified 200× from user writes. Timeline state fits comfortably in RAM. Conclusion: precompute timelines in memory, but cap fan-out amplification for celebrities.

03API design

REST over HTTPS at the edge; services speak gRPC internally. Cursor pagination everywhere — offsets don't survive a moving feed.

# Post a tweet (idempotency key dedupes client retries)
POST /v1/tweets
{ "text": "hello world", "media_ids": ["m_9f2c"], "reply_to": null,
  "idempotency_key": "c1a2-…" }
→ 201 { "tweet_id": "1849236481024917504", "created_at": … }

# Home timeline — cursor = last seen tweet ID (snowflake IDs are time-sortable)
GET /v1/timeline/home?cursor=1849236481024917504&count=20
→ 200 { "tweets": [ …hydrated tweets… ], "next_cursor": "…" }

GET  /v1/users/{id}/tweets?cursor=…&count=20   # user timeline
POST /v1/users/{id}/follow                     # idempotent
DELETE /v1/users/{id}/follow
POST /v1/tweets/{id}/like                      # counter, eventually consistent
POST /v1/tweets/{id}/retweet
GET  /v1/search?q=…&cursor=…                   # tweet search
GET  /v1/trends?place_id=…                     # trending topics

04High-level architecture

Stateless services behind a gateway; synchronous work kept to the minimum, everything amplifying a write pushed through Kafka to async consumers.

CLIENTS EDGE ONLINE SERVICES ASYNC PIPELINE STORAGE & CACHE Mobile apps iOS · Android Web client SPA 3rd-party API OAuth apps CDN media · static Load balancer L4/L7 · TLS API Gateway auth · rate limit · route Tweet Service write path · ID gen · persist · publish Timeline Service read path · merge · hydrate User / Social Graph Service profiles · follow edges Search Service query fan-out · rank Media Service presigned uploads · transcode Engagement Service likes · retweets · counters Tweet store sharded MySQL / Manhattan Graph store follows · FlockDB-style Timeline cache Redis cluster · lists Tweet cache Memcached · hot tweets Object store S3 · media blobs Search index Elasticsearch / Earlybird Counter store Redis + rollup DB Kafka tweet_created · engagement Fan-out workers push to follower timelines Index & trends workers search ingest · heavy hitters Notification workers push · mentions · follows persist read timeline publish events write timelines index
Clients & edge Online services (stateless) Storage & cache Async / streaming (dashed = eventual)
Fig 1 — System overview. The synchronous path (solid) does the minimum: authenticate, persist, respond. Everything that amplifies a write — fan-out, indexing, trends, notifications — hangs off Kafka (dashed) and is eventually consistent.

Component responsibilities

ComponentRoleKey decisions
API GatewayAuth, rate limiting, routing, TLS terminationToken-bucket limits per user; stateless, scaled horizontally.
Tweet ServiceValidate → generate Snowflake ID → persist → publish tweet_createdReturns 201 after durable persist + Kafka ack — fan-out is not awaited.
Timeline ServiceServe home & user timelinesReads only from cache in the hot path; DB fallback for cold users.
Social Graph ServiceFollow edges, both directionsStores followers(user) and following(user) as separate sharded adjacency lists — fan-out needs the former, merge-reads the latter.
Fan-out workersConsume tweet_created, push tweet ID into follower timelinesSkip celebrities; skip inactive users; batch Redis pipeline writes.
Engagement ServiceLikes / retweets / reply countersRedis increments + periodic rollup to durable store; counts are approximate in real time.

05The write path: hybrid fan-out

The defining problem. Push (fan-out-on-write) gives cheap reads but explodes for celebrities; pull (fan-out-on-read) gives cheap writes but makes every read expensive. Twitter does both.

Push — fan-out on write

  • On tweet: insert its ID into every follower's cached timeline
  • Read = one Redis list fetch → fast, O(1)
  • Cost: 100 M followers × 1 tweet = 100 M writes 💥
  • Used for: the ~99.9 % of users with normal follower counts

Pull — fan-out on read

  • On tweet: write once to the author's own list, nothing else
  • Read = fetch tweets of everyone you follow, merge — O(following)
  • Cost shifts to every single timeline load
  • Used for: celebrity accounts (> ~1 M followers) and cold/inactive readers
Author POST /v1/tweets Tweet Service ID · validate · persist Tweet store durable write ① Kafka tweet_created ② Fan-out worker consume ③ Graph Service get followers ④ followers > 1 M ? ⑤ Follower timelines Redis: LPUSH tweet_id to each active followers only · cap 800 Celebrity list only write to author's own timeline followers merge at read time Retry / DLQ at-least-once + idempotent 201 Created (before fan-out) no → push ⑥a yes → skip ⑥b on failure
Fig 2 — Hybrid fan-out on write. The author gets a 201 the moment the tweet is durable and the event is acked (① – ②). Workers fan out asynchronously (③ – ⑥): normal accounts are pushed into each active follower's Redis list; celebrity tweets are written once and merged at read time. Delivery is at-least-once — LPUSH is guarded by a dedupe check so retries are idempotent.

06The read path: serving a home timeline

1.2 M requests/s at peak, p99 under 200 ms. The invariant: the hot path touches only memory.

Reader GET /timeline/home Timeline Service merge · hydrate · trim Redis: home timeline ① LRANGE — 800 precomputed IDs Celebrity merge ② recent IDs of followed celebs Memcached: tweets ③ hydrate IDs → objects (~99 % hit) Tweet store cache-miss fallback Ranking hook optional ML re-order ④ on miss ⑤ 20 hydrated tweets + next_cursor
Fig 3 — Read path. Fetch the precomputed ID list (①), merge in celebrity tweets by time-sorted Snowflake ID (②), hydrate IDs into full objects from Memcached (③), optionally re-rank (④), return a page (⑤). Steps ① – ③ are all in-memory; the DB appears only on cache misses.

07Data model & storage choices

Snowflake IDs — 64 bits, time-sortable, coordination-free

Every tweet needs a unique ID minted at 17 k/s across many hosts. Auto-increment can't be sharded; UUIDs aren't sortable. Snowflake packs time into the high bits so sorting by ID is sorting by time — which is what makes cursor pagination and timeline merging trivial.

41 bitstimestamp (ms since epoch) — ~69 years
5 bitsdatacenter
5 bitsworker
12 bitssequence / ms
4,096 IDs per millisecond per worker × 1,024 workers — no central coordinator, no hot spot.

Core tables

tweets    (tweet_id PK, author_id, text, media_ids[], reply_to, created_at)
          -- sharded by tweet_id → uniform write load; author timeline queries
          -- served by a per-author index/cache, not a cross-shard scan

users     (user_id PK, handle UNIQUE, display_name, bio, follower_count,
           following_count, created_at)          -- sharded by user_id

follows   (follower_id, followee_id, created_at)
          -- stored twice, sharded both ways:
          --   by followee_id → "who follows X"  (fan-out)
          --   by follower_id → "who does X follow" (pull merge)

likes     (user_id, tweet_id, created_at)       -- dedupe + counter source

Redis timeline structure

# One capped list per active user — IDs only, newest first
LPUSH timeline:{user_id} {tweet_id}
LTRIM timeline:{user_id} 0 799        # cap at 800 entries
EXPIRE timeline:{user_id} 2592000     # 30 d idle → evict, rebuild on return

Why these stores

DataStoreReasoning
TweetsSharded MySQL / wide-column (Manhattan, Cassandra)Simple key access by ID, massive write scale, no cross-entity transactions needed. Replicated 3×, cross-region async.
Social graphSharded adjacency lists (MySQL/Redis, FlockDB-style)Two access patterns, both "get list by key" — no graph traversals needed, so no graph DB required.
Home timelinesRedis cluster (RAM)1.2 M inserts/s + 1.2 M reads/s of tiny list ops — this is exactly what Redis is for. Loss is acceptable: rebuildable via pull.
MediaObject store + CDNImmutable blobs; CDN serves ~90 %+ of media bytes at the edge.
SearchInverted index (Elasticsearch / Earlybird)Time-partitioned indexes; recent partitions in RAM since most queries target the last hours.

08Deep dives

Search & trending

Engagement counters at scale

Availability & multi-region

Observability — what to watch

SignalHealthyMeaning when it degrades
Fan-out lag (Kafka consumer)< 5 sTimelines going stale; scale workers or shed inactive-user pushes.
Timeline cache hit rate> 99 %DB rebuild storm risk — check Redis evictions and cluster health.
Tweet-hydration hit rate> 98 %Memcached capacity or a cold shard after failover.
p99 home timeline< 200 msUsually a slow celebrity merge or a hot Redis shard.

09Trade-offs & interview talking points

DecisionWe choseWe gave upWhy it's right here
Fan-out strategyHybrid push + pullSimplicity of a single pathPure push melts on celebrities; pure pull makes every read O(following). The split costs one branch and buys both cheap reads and bounded writes.
ConsistencyEventual (seconds)Read-your-follower's-write immediacyFeed staleness is invisible at 5 s; the RTT and coordination cost of strong consistency is not. Author always sees their own tweet instantly (write-through to own timeline).
Timeline storageIDs in Redis, capped at 800Infinite scroll of precomputed feed50× RAM savings, one canonical tweet copy; < 0.1 % of sessions scroll past 800 entries — those fall back to pull.
Delivery semanticsAt-least-once + idempotent insertExactly-once machineryA dedup check on LPUSH is cheap; distributed exactly-once is not. Missing a tweet is worse than a redundant retry.
CountersApproximate, sharded, async rollupExact real-time countsHot-key correctness at thousands of writes/sec would serialize on a row lock for a number nobody verifies.

Extension questions to expect