Design Twitter / X
Problem Statement
Design Twitter's core functionality — users post tweets, follow other users, and see a timeline feed showing tweets from people they follow. The system must handle celebrities with millions of followers.
Requirements
Functional
- ✓Users can post tweets (text, images, videos)
- ✓Users can follow/unfollow other users
- ✓Home timeline shows tweets from followed users, reverse-chronologically
- ✓Search tweets by keyword
- ✓Like, retweet functionality
Non-Functional
- ✓300M DAU
- ✓Timeline load under 200ms
- ✓5000 tweets written per second
- ✓500,000 timeline reads per second
- ✓High availability — the feed can be slightly stale
Capacity Estimation
Capacity Estimation
- Write QPS: 5,000 tweets/sec
- Read QPS: 500,000 timeline reads/sec → 100:1 read-write ratio
- Tweet storage: avg tweet = 300 bytes. 5K × 86400 × 365 × 5 = ~2.4 TB/year
- Media: most tweets have media. Stored separately on a CDN (e.g., S3 + CloudFront)
- Timeline cache: store ~800 tweets per user in Redis. 300M users × 800 × 8 bytes ≈ 1.9 TB Redis
High-Level Architecture
Architecture
[Write Path]
User → API Gateway → Tweet Service
│
Kafka (tweet events)
│
┌───────────┴────────────┐
│ │
Fan-out Service Search Indexer
│ (Elasticsearch)
injects tweet into
followers' timeline caches
[Read Path]
User → API Gateway → Timeline Service → Redis Cache → DB fallback
Fan-out on write (push model): when a tweet is posted, a background worker writes it to all followers' timeline caches. Timeline reads are O(1) from Redis.
Feed ranking as a separate concern
Feed generation (which posts land in a timeline, via fan-out) and feed ranking (what order they're shown in) are deliberately separate concerns. The fan-out mechanism above populates a timeline cache with candidate post IDs in roughly chronological order — a separate ranking service re-orders (or re-scores) that candidate set at read time, based on signals like recency, engagement, and affinity to the viewing user. Keeping these separate means the ranking algorithm (which evolves constantly, often ML-driven) can be swapped or A/B tested without touching the fan-out/storage architecture at all — this is exactly where an ML ranking model would plug in, as a post-processing step over the candidate set fan-out already produced, not as part of fan-out itself.
Storage choice: posts vs feed cache
Posts themselves (full content) live in a durable store (see the general storage-choice discussion in Design a Chat Application's database design). The feed cache is a distinct, separate structure: Redis sorted sets, keyed per user, scored by timestamp (or a ranking score) — storing only post IDs, not full content. Rendering a feed means reading the sorted set for the ID list, then batch-fetching full post content from the durable store (or a post-content cache) for just that page's IDs — separating "which posts, in what order" from "the posts' actual content" keeps the frequently-read, frequently-reordered structure small and fast.
Pagination: cursor-based vs offset-based
Offset-based (LIMIT 20 OFFSET 40) is simple but breaks under concurrent writes — if new posts arrive between two page requests, offset-based pagination can skip or duplicate items, since "item at position 40" shifts as new items are inserted. Cursor-based pagination instead uses an opaque pointer (typically the last-seen item's score/timestamp) as the starting point for the next page — "give me items older than cursor X" is stable regardless of how many new items arrived in between, which is why every major feed product uses cursor-based pagination, not offset-based, for infinite scroll.
API Design
API Design
POST /api/v1/tweets
Body: { text, mediaIds? }
Response: { tweetId, createdAt }
GET /api/v1/timeline/home?cursor=&limit=20
Response: { tweets: [...], nextCursor }
GET /api/v1/tweets/{tweetId}
POST /api/v1/users/{userId}/follow
DELETE /api/v1/users/{userId}/follow
GET /api/v1/search?q=keyword&cursor=
Database Design
Database Design
Tweets (Cassandra — append-only, time-series)
tweets
tweet_id UUID PK
user_id UUID
content TEXT
media_urls LIST<TEXT>
created_at TIMESTAMP
like_count COUNTER
User social graph (dedicated graph store or Cassandra)
followers
user_id UUID PK
follower_id UUID
following
user_id UUID PK
followee_id UUID
Timeline cache (Redis Sorted Set, score = tweet timestamp)
timeline:{userId} → ZSet of {tweetId: timestamp}
Scaling Strategy
Scaling Strategy
The Celebrity Problem (hot write path)
Fan-out on write breaks for celebrities (e.g., Obama with 130M followers). Writing to 130M timeline caches per tweet takes ~10 seconds.
Solution — Hybrid fan-out:
- Regular users (< 1M followers): fan-out on write (push to timelines)
- Celebrities (≥ 1M followers): fan-out on read — inject celebrity tweets at read time
At read time, the timeline service merges:
- Pre-computed timeline cache (regular users' tweets)
- Recent celebrity tweets fetched from their profile at read time
Timeline cache eviction
Retain only the 800 most recent tweet IDs per user. Older tweets loaded from DB on scroll.
Geo-sharding for a global user base
Partitioning data by the user's geographic region so reads/writes happen physically close to where the user actually is, reducing cross-continent latency — a user in Tokyo reading/writing to a Tokyo-region shard avoids round-tripping to a US-based datacenter on every request. The cost: cross-region queries (e.g. a global trending-topics feature spanning all regions) become genuinely more complex, needing to fan out across regions and aggregate, rather than a single local query.
Directory-based sharding
An alternative to pure hash/range sharding: a central lookup service explicitly maps shard keys (e.g. user ID) to physical shards, rather than deriving the mapping algorithmically from a hash function. This gives full, explicit control over placement — exactly what geo-sharding needs ("this user's data should live in the EU shard specifically," a business/compliance requirement a pure hash function can't express) — at the cost of that directory service itself becoming a potential bottleneck or single point of failure if it isn't made highly available in its own right (typically via its own replication and caching, since it's read far more often than written).
Trade-offs
- Fan-out on write (push): O(followers) on write, O(1) on read. Great for read-heavy systems, bad for celebrities.
- Fan-out on read (pull): O(1) write, O(following_count) read. Simple but slow timelines.
- Hybrid: best of both worlds but most complex to implement.
Bottlenecks
- Hot celebrity accounts — handled by hybrid fan-out
- Redis memory — LRU eviction; cold users' caches are rebuilt from DB on next login
- Kafka consumer lag — add consumers to the fan-out service; scale horizontally
Failure Scenarios
- Kafka down: tweet still saved to DB. Fan-out delayed. Timeline slightly stale — acceptable.
- Redis down: timeline service falls back to DB. Slower but correct.
- Fan-out service overload: shed load for non-priority users, prioritise active users.