Twitter System Design - 1 Hour Interview Guide
Design a scalable microblogging platform like Twitter/X. Covers requirements, capacity estimation, tweet creation, home timeline, fan-out strategies, trending topics, search, notifications, database design, caching, and trade-offs for a 1-hour system design interview.
1-Hour Interview Roadmap
| Time | Topic |
|---|---|
| 0 – 5 min | Requirements clarification |
| 5 – 10 min | Capacity estimation |
| 10 – 18 min | High-level architecture + core components |
| 18 – 28 min | Tweet creation + fan-out strategy |
| 28 – 38 min | Home timeline — assembly + caching |
| 38 – 46 min | Search + trending topics |
| 46 – 54 min | Database design + scaling |
| 54 – 60 min | Trade-offs + failure scenarios |
What Are We Building?
A real-time microblogging platform where users post short messages (tweets), follow other users, and see a reverse-chronological timeline of tweets from people they follow.
Scale reference: Twitter/X has 350 million monthly active users, 200 million daily active users, 500 million tweets posted per day, and serves 800 million timeline reads per day. The defining challenge is the asymmetric follow model — celebrities with 100M+ followers post tweets that must appear in millions of timelines within seconds.
Step 1 — Requirements
Functional Requirements
| # | Requirement |
|---|---|
| 1 | Users can post tweets (text up to 280 characters, optional media) |
| 2 | Users can follow and unfollow other users |
| 3 | Users see a home timeline — tweets from accounts they follow, reverse-chrono |
| 4 | Users can view any public user's profile timeline |
| 5 | Users can like, retweet, quote-tweet, and reply to tweets |
| 6 | Users can search tweets and users by keyword or hashtag |
| 7 | System shows trending topics (hashtags) — globally and by region |
| 8 | Users receive notifications for likes, retweets, replies, new followers |
| 9 | Tweets can include photos, videos, polls, and links |
| 10 | Users can create and join Twitter Spaces (live audio — out of scope) |
Non-Functional Requirements
| # | Requirement |
|---|---|
| 1 | Home timeline load latency < 200ms (p99) |
| 2 | Tweet posted must be visible to followers within 5 seconds |
| 3 | High availability — 99.99% uptime |
| 4 | Eventual consistency acceptable for timeline and counts |
| 5 | Strong consistency for tweet creation — tweet must be durable once ACKed |
| 6 | System must handle extreme skew — users with 100M+ followers |
| 7 | Search must support real-time indexing — new tweets searchable within seconds |
| 8 | Trending topics updated every 5 minutes |
Out of Scope
- Twitter Spaces (live audio)
- Twitter Blue / subscription features
- Ads and promoted content
- Content moderation and trust & safety pipeline
Step 2 — Capacity Estimation
Assumptions
| Metric | Value |
|---|---|
| Daily Active Users (DAU) | 200 million |
| Monthly Active Users (MAU) | 350 million |
| Tweets per day | 500 million |
| Average tweet size (text+meta) | 300 bytes |
| Media tweets (%) | 30% |
| Average media size | 500 KB |
| Average follows per user | 300 |
| Timeline loads per user/day | 10 |
| Tweets shown per load | 20 |
| Read-to-write ratio | ~100:1 |
Write Volume
500 million tweets/day
= ~5,800 tweets/second (sustained)
= ~15,000 tweets/second (peak)
Timeline Read Volume
200M DAU × 10 loads/day = 2 billion timeline reads/day
= ~23,000 reads/second (sustained)
= ~70,000 reads/second (peak)
Fan-out Volume
Average user: 300 followers
500M tweets/day × 300 = 150 billion fan-out writes/day
= ~1.7 million fan-out writes/second (sustained peak)
Storage
Text: 500M/day × 300 bytes = 150 GB/day
Media: 150M/day × 500 KB = 75 TB/day
5-year text storage: ~270 TB
5-year media storage: ~135 PB
Key Insight
Twitter is read-heavy and write-fan-out-heavy. The home timeline is the core product. The central design challenge is the same as Instagram/Facebook: how do you deliver a celebrity's tweet to 100 million follower timelines within 5 seconds? — but with a tighter latency SLA and a purely chronological (not ranked) timeline.
Step 3 — High-Level Architecture
flowchart TD
Client --> AG[API Gateway\nAuth + Rate Limiting]
AG --> TS[Tweet Service]
AG --> TLS[Timeline Service]
AG --> US[User Service]
AG --> SS[Search Service]
AG --> TRD[Trending Service]
AG --> NS[Notification Service]
TS --> KF[Kafka\ntweet.created events]
TS --> TDB[(Tweet Store\nCassandra)]
KF --> FO[Fan-out Service]
KF --> IDX[Search Indexer]
KF --> NW[Notification Workers]
KF --> TRDW[Trending Workers]
FO --> TC[(Timeline Cache\nRedis)]
FO --> TLDB[(Timeline Store\nCassandra)]
TLS --> TC
TC -->|miss| TLDB
US --> UDB[(User Store\nPostgreSQL)]
US --> GDB[(Follow Graph\nCassandra)]
SS --> ES[(Elasticsearch\nTweet Index)]
TRD --> TRDB[(Trending Store\nRedis)]
NW --> FCM[FCM / APNs]
Component Responsibilities
| Component | Responsibility |
|---|---|
| API Gateway | JWT auth, rate limiting (writes: 300/15min per user), routing |
| Tweet Service | Validate, persist, and publish tweet creation events |
| Fan-out Service | Write tweet IDs to follower timelines — hybrid push/pull model |
| Timeline Service | Assemble and return home timeline from cache or persistent store |
| User Service | Registration, profile management, follow/unfollow |
| Search Service | Real-time tweet and user search via Elasticsearch |
| Trending Service | Compute and serve trending hashtags by region and globally |
| Notification Svc | Deliver async like/retweet/reply/follow notifications |
| Search Indexer | Consume tweet events from Kafka, index into Elasticsearch |
| Trending Workers | Consume tweet events, count hashtag frequency, update trending store |
Step 4 — Tweet Creation Flow
What Happens When You Post a Tweet
sequenceDiagram
participant User
participant TS as Tweet Service
participant TDB as Tweet Store (Cassandra)
participant KF as Kafka
participant FO as Fan-out Service
participant TC as Timeline Cache (Redis)
User->>TS: POST /tweets {text, media_url}
TS->>TS: Validate (length, rate limit, spam check)
TS->>TS: Assign tweet_id via Snowflake ID
TS->>TDB: Persist tweet
TDB-->>TS: OK
TS-->>User: ACK {tweet_id, created_at}
TS->>KF: Publish tweet.created {tweet_id, author_id, timestamp}
KF->>FO: Consume event
FO->>GDB: GET followers(author_id) — paginated
loop Per batch of 1,000 followers
FO->>TC: ZADD timeline:{follower_id} score=timestamp tweet_id
end
Key design decisions:
- User gets ACK before fan-out begins — tweet is durable, fan-out is async
- Snowflake ID ensures tweet IDs are globally unique and time-sortable
- Fan-out via Kafka — decouples tweet persistence from timeline delivery
Tweet Validation Rules
| Check | Rule | Response on failure |
|---|---|---|
| Length | Text ≤ 280 characters | 400 Bad Request |
| Rate limit | 300 tweets per 15 minutes per user | 429 Too Many Requests |
| Duplicate detection | Same text from same user within 30 seconds | 409 Conflict |
| URL expansion | Convert short URLs to canonical form | Applied silently |
| Media attachment | Max 4 images or 1 video | 400 if exceeded |
Media Upload Flow
Twitter follows the same pre-signed upload pattern as Instagram:
flowchart LR
User -->|1. Request upload token| MS[Media Service]
MS -->|2. Pre-signed S3 URL| User
User -->|3. Upload directly to S3| S3[Object Storage]
S3 -->|4. Complete| MS
MS -->|5. Return media_url| User
User -->|6. POST tweet with media_url| TS[Tweet Service]
S3 --> CDN[CDN Edge]
Step 5 — Fan-out Strategy
The Celebrity Problem on Twitter
Twitter's follow model is asymmetric (like Instagram, unlike Facebook's bidirectional friends). This creates extreme skew:
| User type | Followers | Fan-out writes per tweet |
|---|---|---|
| Regular user | ~300 | 300 writes |
| Micro-influencer | ~50K | 50,000 writes |
| Celebrity (e.g., Elon Musk) | 150M+ | 150 million writes |
A single tweet from a 150M-follower user cannot fan out synchronously — it would take hours.
Hybrid Fan-out Model
flowchart TD
TweetEvent -->|author follower count| CHK{Follower\nCount}
CHK -->|≤ 1M followers\nRegular + Influencer| PUSH[Fan-out on WRITE\nPush tweet_id to all\nfollower timeline caches\nasync, batched]
CHK -->|> 1M followers\nCelebrity| SKIP[Skip fan-out write\nDo NOT push to timelines]
TimelineLoad[Follower loads timeline] --> ASSM[Timeline Assembler]
ASSM -->|Pre-built cache| RegTweets[Regular accounts' tweets\nfrom Redis timeline cache]
ASSM -->|Live pull| CelebTweets[Celebrity tweets\nfetch from Tweet Store\nfor each celebrity followed]
ASSM -->|Merge| Final[Sort by timestamp DESC\nReturn top 20]
Push path — regular users (≤ 1M followers)
- Fan-out Service consumes
tweet.createdfrom Kafka - Fetches follower list from social graph in batches of 1,000
- For each follower:
ZADD timeline:{follower_id} {timestamp} {tweet_id}in Redis - Each timeline cache holds the most recent 800 tweet IDs per user
Pull path — celebrities (> 1M followers)
- No writes to follower caches — celebrity tweets are never pushed
- Twitter maintains a separate Celebrity Hot Cache per user
- When Bob loads his timeline, Timeline Service:
- Fetches Bob's pre-built timeline from Redis (regular accounts)
- Fetches the last 20 tweets from each celebrity Bob follows (from Tweet Store)
- Merges both sets and sorts by
tweet_id(time-ordered) descending
Why 1M as the threshold?
1M followers × 15,000 peak tweets/sec from high-follower accounts
= 15 billion fan-out writes/second — impossible
With hybrid model:
Only users < 1M push-fan-out
Celebrities are ~0.001% of accounts but create 20%+ of timeline views
Fan-out Parallelism
flowchart LR
KF[Kafka\ntweet.created\n200 partitions] --> FO1[Fan-out Worker 1]
KF --> FO2[Fan-out Worker 2]
KF --> FO3[Fan-out Worker N]
FO1 -->|Redis pipeline\n1000 ZADD/batch| RC[(Redis Cluster)]
FO2 --> RC
FO3 --> RC
- Kafka partitioned by
author_id— all events from one author go to one partition - Fan-out workers each own a set of Kafka partitions
- Redis pipeline batching: send 1,000 ZADD commands in a single round trip
Step 6 — Home Timeline Assembly
Timeline Cache Structure (Redis Sorted Set)
Key: timeline:{user_id}
Type: Sorted Set
Score: tweet_id (Snowflake — encodes timestamp)
Member: tweet_id
ZREVRANGE timeline:12345 0 19 → [tweet_id_999, tweet_id_998, ..., tweet_id_980]
- Score = tweet_id (not timestamp) — Snowflake IDs are already time-ordered
ZREVRANGEreturns most recent tweets first- Each user's timeline holds at most 800 tweet IDs
- TTL: 48 hours — inactive users' caches are evicted; rebuilt on next login
Timeline Read Flow
sequenceDiagram
participant Bob
participant TLS as Timeline Service
participant TC as Timeline Cache (Redis)
participant TLDB as Timeline Store (Cassandra)
participant RANK as Celebrity Merger Service
participant TDB as Tweet Store (Cassandra)
participant PC as Post Cache (Redis)
Bob->>TLS: GET /home_timeline?count=20
TLS->>TC: Fetch timeline ZSET
alt Cache hit
TC-->>TLS: tweet_id list
else Cache miss
TLS->>TLDB: Query timeline table
TLDB-->>TLS: tweet_id list
TLS->>TC: Rebuild cache
end
TLS->>RANK: Fetch celebrity tweets
RANK->>TDB: Query top tweets per celebrity
TDB-->>RANK: tweet IDs
RANK-->>TLS: merged tweet IDs
TLS->>PC: Fetch cached posts
PC-->>TLS: cached posts (partial)
TLS->>TDB: Fetch missing posts
TDB-->>TLS: full tweet data
TLS-->>Bob: Final timeline response (20 tweets)
Pagination
Cursor-based, not offset-based:
First load: GET /home_timeline?count=20
Response: { tweets: [...], next_cursor: "tweet_id_XYZ" }
Next page: GET /home_timeline?max_id=tweet_id_XYZ&count=20
Response: { tweets: [...older tweets...], next_cursor: "..." }
max_id is the oldest tweet_id seen — return all tweets with ID < max_id. This is stable even when new tweets arrive at the top.
User Timeline (Profile Page)
When viewing someone's profile, return their tweets directly:
GET /users/:username/tweets
Query: SELECT * FROM tweets WHERE author_id = ? ORDER BY tweet_id DESC LIMIT 20
No fan-out needed — profile timeline reads directly from the Tweet Store partitioned by author_id.
Step 7 — Tweet Store and Database Design
Database Selection
| Data | Database | Reason |
|---|---|---|
| Tweets | Cassandra | Write-heavy, time-ordered, append-only, billions of rows |
| Home timeline | Redis (hot) + Cassandra (cold) | Fast sorted set reads; durable backup |
| Follow graph | Cassandra | Wide rows — billions of edges, fast range read by user_id |
| Users & profiles | PostgreSQL | ACID, structured, strong consistency |
| Likes & retweets | Cassandra | Very high write volume, append-only |
| Search index | Elasticsearch | Full-text, hashtag, real-time indexing |
| Trending counters | Redis | INCR per hashtag per time window — atomic, fast |
| Notifications | Cassandra | Append-only, high volume |
Tweet Table (Cassandra)
Table: tweets
Partition key: author_id BIGINT
Clustering key: tweet_id BIGINT DESC (Snowflake — newest first)
Columns:
text TEXT
media_urls LIST<TEXT> (CDN URLs)
reply_to_id BIGINT (null if original tweet)
retweet_of_id BIGINT (null if original tweet)
like_count COUNTER
retweet_count COUNTER
reply_count COUNTER
quote_count COUNTER
hashtags SET<TEXT>
mentions SET<BIGINT> (user_ids of @mentions)
language VARCHAR
is_deleted BOOLEAN
created_at TIMESTAMP
Query patterns:
- Profile timeline:
WHERE author_id = ? ORDER BY tweet_id DESC LIMIT 20 - Single tweet fetch:
WHERE author_id = ? AND tweet_id = ? - Batch fetch (timeline hydration):
WHERE (author_id, tweet_id) IN (...)
Follow Graph (Cassandra)
Table: followers (who follows this user — for fan-out)
user_id BIGINT
follower_id BIGINT
created_at TIMESTAMP
PRIMARY KEY (user_id, follower_id)
Table: following (who this user follows — for timeline assembly)
user_id BIGINT
followed_id BIGINT
created_at TIMESTAMP
is_celebrity BOOLEAN (true if followed_id has > 1M followers)
PRIMARY KEY (user_id, followed_id)
The is_celebrity flag is denormalized onto the following table — Timeline Service reads it to know which accounts to pull vs use from cache.
Timeline Store (Cassandra)
Table: home_timeline
Partition key: user_id BIGINT
Clustering key: tweet_id BIGINT DESC (time-ordered, newest first)
Columns:
author_id BIGINT
inserted_at TIMESTAMP
PRIMARY KEY (user_id, tweet_id)
- Acts as cold storage for users who are inactive (cache miss)
- Holds up to 3,000 tweet IDs per user — older entries are TTL-expired
Like and Retweet Tables (Cassandra)
Table: likes
tweet_id BIGINT
user_id BIGINT
liked_at TIMESTAMP
PRIMARY KEY (tweet_id, user_id) — unique per (tweet, user)
Table: retweets
tweet_id BIGINT
retweeter_id BIGINT
retweet_tweet_id BIGINT (the new tweet_id created for the retweet)
created_at TIMESTAMP
PRIMARY KEY (tweet_id, retweeter_id)
Step 8 — Search
Requirements
- Search tweets by keyword, hashtag, or phrase
- Search users by username or display name
- Results must include tweets posted within the last few seconds
- Support autocomplete on hashtags and usernames
Why Elasticsearch
| Feature | Elasticsearch | Cassandra / PostgreSQL |
|---|---|---|
| Full-text search | Native inverted index | LIKE '%keyword%' = full table scan |
| Hashtag search | Term query — instant | Requires separate index table |
| Real-time indexing | Near real-time (< 1 second NRT) | Not designed for search |
| Relevance ranking | BM25 scoring built-in | Not available |
| Autocomplete | search_as_you_type field type |
Not supported |
| Horizontal scale | Sharding + replication built-in | Difficult for search workloads |
Real-time Indexing Pipeline
flowchart LR
KF[Kafka\ntweet.created] --> IDX[Search Indexer\nService]
IDX --> ES[Elasticsearch\nTweet Index]
ES -->|< 1 second NRT| SearchAPI[Search API]
- Search Indexer consumes
tweet.createdevents from Kafka - Indexes tweet text, author, hashtags, timestamp, language, location (if provided)
- Near Real-time (NRT): Elasticsearch refreshes its index every 1 second by default
- New tweets are searchable within ~2 seconds of posting
Tweet Index Schema (Elasticsearch)
{
"tweet_id": "long",
"author_id": "long",
"author_name": "search_as_you_type",
"text": "text (analyzed, language-aware)",
"hashtags": "keyword[]",
"mentions": "long[]",
"language": "keyword",
"created_at": "date",
"like_count": "long",
"retweet_count": "long",
"location": "geo_point"
}
Search Flow
flowchart LR
User -->|"#SystemDesign"| SS[Search Service]
SS -->|bool query\nhashtags:SystemDesign| ES[Elasticsearch]
ES -->|Top 50 matching tweet IDs + scores| SS
SS -->|Batch fetch full tweets| TC[(Redis\nTweet Cache)]
TC -->|misses| TDB[(Cassandra\nTweet Store)]
SS --> User
Trending Topics
How Trending Works
flowchart LR
KF[Kafka\ntweet.created] --> TW[Trending Workers]
TW -->|ZINCRBY trending:global 1 hashtag| RC[(Redis\nSorted Sets)]
TW -->|ZINCRBY trending:us 1 hashtag| RC
TW -->|ZINCRBY trending:in 1 hashtag| RC
RC -->|ZREVRANGE trending:global 0 9| TAPI[Trending API]
TAPI --> User
Trending Algorithm
Twitter uses time-windowed counting — not raw total volume, but rate of increase:
Trending Score = (tweets_in_last_30_min / tweets_in_last_6_hours) × recency_factor
A topic that suddenly spikes from 100 tweets/hour to 50,000 tweets/hour trends immediately, even if another topic has 1 million total tweets.
Redis Sliding Window Implementation
Every 5 minutes, rotate time bucket:
Key: trending:{region}:{time_bucket}
Type: Sorted Set
Op: ZINCRBY trending:global:{bucket} 1 "#SystemDesign"
Trending score = sum of counts across last 6 × 5-min buckets
weighted by recency (recent buckets weighted higher)
Top 10: ZREVRANGE trending:global:{current_bucket} 0 9
Old buckets are expired automatically via TTL (6 hours).
Step 9 — Notifications
Notification Events
| Trigger Event | Recipient(s) | Priority |
|---|---|---|
| Like on tweet | Tweet author | Low |
| Retweet | Original tweet author | Medium |
| Reply | Tweet author + mentioned users | High |
| Quote tweet | Original tweet author | Medium |
| New follower | The user being followed | Medium |
| @mention in tweet | Mentioned user | High |
| Direct Message | Recipient | High |
Async Notification Pipeline
flowchart LR
TS[Tweet Service] -->|tweet.created| KF[Kafka]
LKS[Like Service] -->|like.created| KF
FLW[Follow Service] -->|follow.created| KF
KF --> NW[Notification Workers]
NW -->|Check preferences| PREF[(Redis\nUser Prefs)]
NW -->|User online?| SESS[(Redis\nSessions)]
NW -->|Online| WS[WebSocket / SSE\nIn-app notification]
NW -->|Offline| FCM[FCM / APNs\nPush notification]
NW -->|Store| NDB[(Cassandra\nNotification Store)]
Notification Batching
For high-volume events (a viral tweet getting 10,000 likes in a minute):
- Do not send 10,000 individual push notifications to the author
- Batch: send "You have 10,000+ new likes" every 5 minutes
- Redis counter tracks unsent notification count per user per event type
- Flush and notify on a schedule, not per-event
Step 10 — Caching Strategy
Redis Cache Map
| Cache | Key Pattern | TTL | Contents |
|---|---|---|---|
| Home timeline | timeline:{user_id} |
48 hrs | Sorted Set — tweet_id → score (time-ordered) |
| Tweet details | tweet:{tweet_id} |
7 days | Full tweet object (text, counts, media) |
| User profile | user:{user_id} |
1 hr | Profile data, follower/following counts |
| Like check | liked:{tweet_id}:{user_id} |
1 hr | Boolean — did user like this tweet? |
| Like count | likes:{tweet_id} |
Eventual | Counter — async flushed to Cassandra |
| Retweet count | retweets:{tweet_id} |
Eventual | Counter |
| Celebrity recent tweets | celeb_tweets:{user_id} |
2 min | Last 50 tweet IDs — for pull-path merge |
| Trending (global) | trending:global |
5 min | Sorted Set — hashtag → trending score |
| Trending (by region) | trending:{region} |
5 min | Sorted Set per region |
| Follow graph (hot) | following:{user_id} |
30 min | List of followed user IDs (for timeline merge) |
| Active sessions | session:{user_id} |
30 days | Device tokens for push notifications |
Viral Tweet Hot Spot
A tweet from Elon Musk gets 5 million likes in 10 minutes. If every like writes to Cassandra:
5,000,000 likes / 600 seconds = ~8,333 writes/second to ONE partition
This creates a Cassandra hot partition that degrades performance.
Solution:
flowchart LR
CLIENT["User Like Action"]
REDIS["Redis Counter (INCR likes)"]
FLUSH["Background Flush Worker"]
DB["Cassandra Persistent Store"]
CACHE["Redis Read Cache"]
CLIENT --> REDIS
REDIS --> FLUSH
FLUSH --> DB
DB --> CACHE
- All like increments hit Redis
INCR— O(1), sub-millisecond, no contention - Async worker flushes accumulated counts to Cassandra every 30 seconds
- Tweet reads always served from Redis cache — Cassandra never hot-spotted on reads
Step 11 — Rate Limiting
Twitter has aggressive rate limits to prevent spam and API abuse.
Write Rate Limits
| Operation | Limit per user | Window |
|---|---|---|
| Post tweet | 300 | 15 min |
| Follow user | 400 | 24 hrs |
| Like tweet | 1,000 | 24 hrs |
| Retweet | 300 | 24 hrs |
| Direct message | 500 | 24 hrs |
API Read Rate Limits (Developer API)
| Endpoint | Basic tier | Enterprise tier |
|---|---|---|
| Home timeline | 15 req / 15 min | 900 req / 15 min |
| User tweets | 15 req / 15 min | 1,500 req / 15 min |
| Search tweets | 180 req / 15 min | 450 req / 15 min |
Rate Limiting Architecture
flowchart LR
Client --> AG[API Gateway]
AG -->|Sliding window check| RC[(Redis\nRate Limit Counters)]
RC -->|Under limit| Service[Downstream Service]
RC -->|Over limit| ERR[429 Too Many Requests\nRetry-After header]
Algorithm: Token Bucket or Sliding Window Counter in Redis.
Key: rate:{user_id}:{endpoint}:{window}
Op: INCR + EXPIRE
Check: if count > limit → reject with 429
Step 12 — Scaling
Read Scaling (Timeline)
flowchart TD
LB[Load Balancer] --> T1[Timeline Pod]
LB --> T2[Timeline Pod]
LB --> T3[Timeline Pod]
T1 & T2 & T3 --> RC[(Redis Cluster\nTimeline Cache)]
RC -->|miss| CS[(Cassandra\nTimeline Store + Tweet Store)]
- Timeline Service is stateless — add pods horizontally
- Redis Cluster distributes timeline keys across shards
- Cassandra read replicas absorb cache misses
Write Scaling (Fan-out)
flowchart LR
KF[Kafka\n200 partitions\nauthor_id key] --> FW1[Fan-out Worker]
KF --> FW2[Fan-out Worker]
KF --> FW3[Fan-out Worker]
FW1 & FW2 & FW3 -->|Redis PIPELINE\n1000 ZADD/batch| RC[(Redis Cluster)]
FW1 & FW2 & FW3 -->|async| CS[(Cassandra\nTimeline Store)]
- Kafka partitioned by
author_id— fan-out for one author always on one partition - Workers process follower batches of 1,000 using Redis pipelining
- Scale fan-out workers independently from tweet creation workers
Database Sharding
| Database | Shard Key | Notes |
|---|---|---|
| Tweets | author_id |
All tweets from one user co-located on one shard |
| Timeline | user_id |
Each user's timeline on one shard |
| Follow graph | user_id |
All edges for one user co-located |
| Likes | tweet_id |
All likes for one tweet on one shard |
| Search | Elasticsearch sharding built-in | Shard by tweet_id range |
Handling the Celebrity Follow Unfollow Problem
When a celebrity (100M followers) is followed by a new user:
- Do not retroactively fan-out their historical tweets to the new follower's cache
- Timeline Service handles this: on first load after a new follow, pull the last 20 tweets from the newly followed celebrity and inject into the response
- Cache warms progressively with subsequent real-time fan-out events
Step 13 — Multi-Region Deployment
flowchart TD
Users --> GLB[Global Load Balancer\nAnyCast DNS]
GLB -->|US users| US[US-East\nPrimary]
GLB -->|EU users| EU[EU-West\nReplica]
GLB -->|APAC users| APAC[APAC\nReplica]
US -->|async replication| EU
US -->|async replication| APAC
| Operation | Strategy |
|---|---|
| Tweet creation | Write to local region, async replicate to others (eventual) |
| Timeline read | Served from local region replica — sub-50ms |
| Search | Elasticsearch cluster per region; tweet index replicated |
| Trending | Per-region trending computed locally; global trending aggregated |
| Consistency | Eventual — a tweet may take 3–10s to appear globally |
Step 14 — Failure Scenarios
| Failure | Impact | Mitigation |
|---|---|---|
| Redis (timeline cache) down | All timeline reads fall to Cassandra | Redis Sentinel / Cluster auto-failover; Cassandra is source of truth |
| Kafka down | Fan-out and indexing pause | 3-broker Kafka cluster; ISR replication; producers retry with backoff |
| Fan-out workers down | New tweets don't fan out to followers | Kafka retains events (7-day retention); catch up on restart |
| Cassandra node down | Partial reads/writes rerouted | RF=3; coordinator retries on healthy replicas |
| Elasticsearch down | Search unavailable | Graceful degradation — show cached trending; search returns 503 |
| Trending service down | Trending tab empty | Serve cached last-known trending list; TTL = 30 min fallback |
| Media upload fails | Tweet posted without media | Pre-signed URL expires in 15 min; client retries; tweet stays DRAFT |
| Hot fan-out (viral user) | Fan-out workers backed up | Kafka consumer lag alert; auto-scale fan-out workers; skip inactive users |
| Rate limiter Redis down | Rate limits unenforced | Fail open (allow traffic) for 60s; Redis Sentinel restores quickly |
| Notification service down | Delayed notifications | Notifications stored in Cassandra first; delivered when service recovers |
Final Architecture
flowchart TD
Mobile[Mobile / Web Clients] --> GLB[Global Load Balancer]
GLB --> AG[API Gateway\nAuth + Rate Limiting]
AG --> TS[Tweet Service]
AG --> TLS[Timeline Service]
AG --> US[User Service]
AG --> SS[Search Service]
AG --> TRD[Trending Service]
TS --> KF[Kafka Cluster\nauthor_id partitioned]
KF --> FO[Fan-out Workers × N]
KF --> IDX[Search Indexer]
KF --> NW[Notification Workers]
KF --> TW[Trending Workers]
FO --> TC[(Redis Cluster\nTimeline Cache)]
FO --> TLDB[(Cassandra\nTimeline Store)]
TLS --> TC
TC -->|miss| TLDB
TLS --> TDB[(Cassandra\nTweet Store)]
US --> UDB[(PostgreSQL\nUser Profiles)]
US --> GDB[(Cassandra\nFollow Graph)]
IDX --> ES[(Elasticsearch\nTweet Index)]
SS --> ES
TW --> TRDB[(Redis\nTrending Counters)]
TRD --> TRDB
NW --> FCM[FCM / APNs]
NW --> NDB[(Cassandra\nNotifications)]
TDB --> CDN[CDN\nMedia Delivery]
Technology Stack
| Layer | Technology |
|---|---|
| API Gateway | NGINX / Envoy / Kong |
| Tweet + Timeline Svc | Scala / Java / Go |
| Message queue | Apache Kafka (200+ partitions) |
| Timeline + trend cache | Redis Cluster |
| Tweet + timeline store | Apache Cassandra |
| User profiles | PostgreSQL (primary + read replicas) |
| Follow graph | Apache Cassandra |
| Search + hashtags | Elasticsearch (6+ shards per index) |
| Object storage | Amazon S3 / Google Cloud Storage |
| Video transcoding | FFmpeg workers / AWS MediaConvert |
| CDN | Fastly / Akamai / CloudFront |
| Push notifications | FCM (Android) + APNs (iOS) |
| Monitoring | Prometheus + Grafana + ELK + Jaeger |
| Deployment | Kubernetes, multi-region |
Key Trade-Offs
| Decision | Option A | Option B | Choice & Reason |
|---|---|---|---|
| Timeline ordering | Chronological | ML-ranked by relevance | Chronological (Twitter's core identity); ML ranking optional "For You" tab |
| Fan-out strategy | Push only | Pull only | Hybrid — push for < 1M followers; pull for celebrities |
| Fan-out timing | Synchronous | Async via Kafka | Async — user gets ACK immediately; fan-out completes in < 5 seconds |
| Timeline storage | Redis only | Redis + Cassandra | Both — Redis for hot users; Cassandra for cold/inactive users |
| Like counts | Exact row-level increment | Redis counter + async flush | Redis counter — prevents Cassandra hotspot on viral tweets |
| Search | Cassandra text scan | Elasticsearch | Elasticsearch — real-time NRT indexing, full-text, hashtag, geo search |
| Trending | Real-time per-event counters | Time-windowed sliding buckets | Sliding window — rate-of-increase matters more than total volume |
| Pagination | Offset-based (?page=2) |
Cursor-based (?max_id=...) |
Cursor — stable under real-time inserts; no duplicate tweets |
| Notification delivery | Synchronous per event | Batched async via Kafka | Async batched — 10K likes should not send 10K push notifications |
Twitter vs Instagram vs Facebook — Design Comparison
| Dimension | Facebook News Feed | ||
|---|---|---|---|
| Social model | Asymmetric follow | Asymmetric follow | Bidirectional friends + pages |
| Timeline ordering | Reverse-chronological | ML-ranked | ML-ranked (EdgeRank) |
| Fan-out threshold | ~1M followers | ~1M followers | ~100K followers |
| Feed complexity | Lower — sort by time | Medium — ML ranking | High — ML + multi-content type |
| Search | Real-time tweet search | User + hashtag search | User + page search |
| Trending topics | Yes — core feature | Explore page | Trending section |
| Content types | Short text + media | Photo + video only | Text, photo, video, link, share |
| Key design challenge | Celebrity fan-out + real-time NRT | Media upload + fan-out | Feed ranking + social graph complexity |
Common Interview Mistakes
- ❌ Not explaining the celebrity fan-out problem and the 1M-follower threshold
- ❌ Designing a synchronous fan-out — blocks tweet creation at peak load
- ❌ Not distinguishing home timeline (fan-out) from user/profile timeline (direct DB read)
- ❌ Using offset pagination — creates duplicate/skipped tweets on a live feed
- ❌ Storing like counts with DB row-level increments — Cassandra hot partition on viral tweets
- ❌ No caching layer — 70K timeline reads/sec cannot be served from Cassandra directly
- ❌ Using LIKE '%keyword%' for search — full table scan at billions of tweets
- ❌ Trending topics based on total volume, not rate of increase — misses what's actually trending
- ❌ No cold-start plan for inactive users whose timeline cache has expired
- ❌ No rate limiting discussion — Twitter's rate limits are a core part of its API design
Interview Questions
- How does Twitter's home timeline differ from a user's profile timeline?
- What is the fan-out problem? How does Twitter handle a tweet from a user with 100M followers?
- Why is fan-out done asynchronously via Kafka?
- How do you decide whether to push a tweet (fan-out on write) or pull it (fan-out on read)?
- How is the home timeline stored in Redis? What data structure is used?
- What happens when an inactive user's timeline cache has expired (cold start)?
- How do you handle the case where a new follower needs to see a celebrity's historical tweets?
- Why is cursor-based (
max_id) pagination better than offset pagination for Twitter? - How is Elasticsearch used for real-time tweet search?
- How do trending topics work? What is the difference between volume and rate-of-increase?
- How do you prevent a viral tweet from hot-spotting a single Cassandra partition?
- How does Twitter rate limiting work technically?
- What happens if the Redis timeline cache cluster goes down?
- How do you ensure tweet ordering is preserved in Cassandra using Snowflake IDs?
- How would you design the "For You" algorithmic timeline tab alongside the chronological tab?
Summary
| Concern | Solution |
|---|---|
| Tweet persistence | Cassandra — partitioned by author_id, clustered by tweet_id DESC |
| Fan-out | Hybrid — push to Redis for ≤1M followers; pull from Tweet Store for celebrities |
| Fan-out pipeline | Async Kafka — tweet ACK returned before fan-out; workers consume in parallel |
| Home timeline cache | Redis Sorted Set per user — tweet_ids scored by Snowflake ID |
| Timeline cold start | Rebuild from Cassandra timeline store; celebrities merged at read time |
| Like/retweet counts | Redis INCR counters — async flush to Cassandra every 30 seconds |
| Search | Elasticsearch — NRT indexing via Kafka; full-text + hashtag + geo |
| Trending topics | Redis sliding window buckets — rate-of-increase, not total volume |
| Notifications | Async Kafka pipeline — batched to avoid 10K push notifications per viral tweet |
| Rate limiting | Redis sliding window counters at API Gateway |
| Media storage | Pre-signed S3 upload + CDN delivery — no media through app servers |
| Scale | Stateless services + Kafka partitioning + Redis Cluster + Cassandra RF=3 |
| Multi-region | Read local, write primary, async replication, eventual consistency (< 10s) |
The core principle: Twitter's home timeline is a fan-out delivery problem. The push-pull hybrid at the 1M-follower threshold, combined with Redis sorted sets for fast assembly, is what makes real-time chronological timelines possible at 200M daily users.