Designing Twitter: Fan-Out Strategies, the Celebrity Problem, and the Home Timeline
Design a Twitter-like social feed. Cover fan-out-on-write vs fan-out-on-read, the celebrity/hot-user problem, follower graph storage, and notification delivery at scale.
Why Study Twitter?
Twitter (X) serves hundreds of millions of users posting hundreds of millions of tweets a day, but the hard part isn't storing tweets — it's the home timeline: showing every user a feed of tweets from everyone they follow, ranked roughly by recency, fast. The follower graph is wildly asymmetric — most accounts have a few hundred followers, but some have over 100 million — and that asymmetry is what makes this a genuinely hard distributed systems problem rather than a CRUD app.
Approach: Interviewers use this to test whether you can reason about a skewed access pattern rather than assume uniform load. The "obvious" design (write a tweet, fan it out to every follower's feed) breaks down catastrophically for accounts with millions of followers — identifying that breakdown and fixing it is the core of this interview.
Requirements Analysis
Functional Requirements
- Post a Tweet: Users compose tweets (text, media, up to a character limit)
- Follow/Unfollow: Users build a directed follower graph
- Home Timeline: See a reverse-chronological (or lightly ranked) feed of tweets from followed accounts
- Notifications: Get notified of likes, retweets, replies, and new followers
- Engagement: Like, retweet, reply
- Search (optional): Search tweets and users
Non-Functional Requirements
| Requirement | Target |
|---|---|
| Scale | 100M+ tweets/day, 500M+ timeline reads/day |
| Latency | Home timeline loads in < 200ms (p99) |
| Availability | 99.99% uptime — feed is the core product surface |
| Follower skew | Must handle accounts with 100M+ followers without degrading writes |
| Consistency | Eventual consistency acceptable for timeline; tweet content itself must be durable |
High-Level Architecture
Deep Dive: Fan-Out-on-Write vs Fan-Out-on-Read vs Hybrid
When a user tweets, that tweet needs to show up in the home timeline of every follower. There are two fundamentally different ways to make that happen, and the choice between them is the central design decision of this system.
| Model | Mechanics | Write Cost | Read Cost | Breaks Down When |
|---|---|---|---|---|
| Fan-out on write (push) | On tweet, immediately write the tweet ID into every follower's precomputed timeline cache | O(followers) writes per tweet | O(1) — just read the cache | A single tweet from a 100M-follower account triggers 100M writes |
| Fan-out on read (pull) | On timeline load, fetch tweets from every followed account and merge-sort them on the fly | O(1) — just append the tweet once | O(following count) reads per timeline load | A user following 5,000 accounts pays that cost on every timeline load |
| Hybrid | Push for accounts below a follower threshold; skip push and merge at read time for accounts above it | O(followers) only for normal accounts | O(celebrities followed) extra merge work | Rarely — this is what production systems converge on |
Twitter's follower graph is the reason this matters more here than almost anywhere else: it's a directed graph with extreme in-degree skew. A typical account has a few hundred followers; a handful of accounts have over 100 million. Fan-out cost is driven entirely by follower count (in-degree), so the design has to specifically target the small number of extreme outliers rather than optimize for the "average" account.
Why not pure push or pure pull? Pure push means a single tweet from a top account generates tens of millions of writes before anyone can see it — an unacceptable write amplification and latency spike. Pure pull means every timeline load, even for a user following only 50 people, does real-time fan-in work across all of them, and that cost scales with how many accounts a user follows, not with how popular the tweeter is. The hybrid model routes each tweet through whichever path is cheap for that account's follower count.
Deep Dive: The Celebrity Problem
Any account above a follower threshold (production systems typically use a cutoff somewhere in the tens of thousands to low millions) is flagged as a "celebrity" account and is exempted from the normal fan-out path.
At read time, the Timeline Service does two things: read the user's precomputed timeline cache (populated by push fan-out from regular accounts they follow), and separately fetch recent tweets from any celebrity accounts they follow (a short, bounded list per user), then merge-sort the two sets by timestamp before returning the page.
| Aspect | Regular Account Path | Celebrity Account Path |
|---|---|---|
| Write cost per tweet | O(followers) — can be millions | O(1) — single write to tweet store |
| Read cost | O(1) cache lookup | Small merge: fetch N celebrity accounts' latest tweets |
| Staleness risk | Timeline cache might lag by seconds under load | None — always reads latest at request time |
| Who counts as a celebrity | N/A | Follower count above a configured threshold, re-evaluated periodically |
This threshold is a tunable, not a fixed law. A user who follows several celebrity accounts pays a small, bounded merge cost per timeline load — bounded because the number of celebrities any one user follows is small even if each celebrity has huge reach. That asymmetry (bound the rare expensive case, optimize the common cheap case) is the general pattern to name explicitly in an interview, not just the specific threshold number.
Deep Dive: Fan-Out Sequence
The write path returns to the user as soon as the tweet is durably stored — fan-out happens asynchronously afterward via the queue, so posting latency is decoupled from follower count entirely, even for regular accounts with tens of thousands of followers.
Database Schema
Tweets Table
CREATE TABLE tweets (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
content VARCHAR(280) NOT NULL,
media_urls TEXT[],
reply_to_id BIGINT,
retweet_of_id BIGINT,
like_count INT DEFAULT 0,
retweet_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_user_created ON tweets(user_id, created_at DESC);
CREATE INDEX idx_reply_to ON tweets(reply_to_id);Follows Table (Follower Graph)
The follower graph is a directed edge list. Two indexes are needed because the graph is queried in both directions: "who does X follow" (fan-out-on-read merge, following list) and "who follows X" (fan-out-on-write target list).
CREATE TABLE follows (
follower_id BIGINT NOT NULL,
followee_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (follower_id, followee_id)
);
CREATE INDEX idx_followee ON follows(followee_id, follower_id);
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(30) UNIQUE NOT NULL,
follower_count INT DEFAULT 0,
is_celebrity BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_is_celebrity ON users(is_celebrity);Timeline Cache Table (Conceptual — Redis-backed)
The precomputed timeline isn't a SQL table in production (it's a Redis sorted set or list per user), but modeling it relationally clarifies the shape of what fan-out writes:
-- Conceptual shape of what lives in Redis as a sorted set per user,
-- keyed by user_id, scored by tweet timestamp.
CREATE TABLE timeline_cache (
user_id BIGINT NOT NULL,
tweet_id BIGINT NOT NULL,
tweet_time TIMESTAMP NOT NULL,
PRIMARY KEY (user_id, tweet_id)
);
CREATE INDEX idx_user_time ON timeline_cache(user_id, tweet_time DESC);Why a sorted set, not a list? A Redis sorted set (ZADD/ZREVRANGE) scored by timestamp gives O(log N) inserts and O(log N + M) range reads for the top M tweets, and naturally caps size — old entries can be trimmed (ZREMRANGEBYRANK) so each user's cached timeline never grows unbounded, which matters since fan-out writes into it continuously.
Notification Delivery Design
Notifications (likes, retweets, replies, new followers) go through the same async queue as fan-out, but fan out to a much smaller, bounded audience (the tweet's author, or a handful of mentioned users) rather than an entire follower list — so the celebrity problem doesn't apply here in the same way.
| Design Decision | Rationale |
|---|---|
| Async delivery via queue | Engagement actions (like, retweet) shouldn't block on notification delivery |
| Batching/deduplication | A viral tweet getting 10K likes/minute becomes one "10K people liked your tweet" notification, not 10K pushes |
| Bounded fan-out target | Notifications target one user (the author) or a small mention list — never the follower graph, so celebrity accounts don't create a notification storm the way tweets create a timeline storm |
| Separate store from timeline | Notification read patterns (unread count, mark-as-read) differ from timeline patterns (infinite scroll by time) |
Caching Strategy
| Cache | What It Holds | TTL / Eviction |
|---|---|---|
| Timeline cache (Redis sorted sets) | Precomputed tweet IDs per user, from push fan-out | Trimmed to last ~800 tweets per user; not time-based |
| Tweet content cache | Hot tweet objects (text, counts) keyed by tweet_id | LRU, short TTL, refreshed on engagement |
| Follower graph cache | Follower/following lists for high-degree accounts | LRU — avoids repeated full graph scans during fan-out |
| Celebrity flag cache | Whether a user is above the fan-out threshold | Refreshed periodically, not per-tweet |
Scaling Challenges & Solutions
| Challenge | Solution |
|---|---|
| Celebrity write amplification | Skip fan-out above a follower threshold; merge celebrity tweets at read time |
| Timeline read latency | Precomputed Redis sorted sets for the common case; small bounded merge for celebrities |
| Follower graph hot rows | Cache high-degree follower/following lists; paginate fan-out in batches |
| Notification storms | Batch and deduplicate engagement notifications rather than one-per-event |
| Fan-out worker backlog | Kafka buffers events; workers scale horizontally and process in parallel batches |
| Timeline cache staleness | Acceptable eventual consistency — a few seconds of lag is invisible to users |
| Unbounded timeline growth | Trim cached timelines to a fixed recent window (e.g., last 800 tweets) |
Key Takeaways
- Fan-out choice follows the graph shape: Push is cheap when follower counts are small; pull is cheap when following counts are small. Twitter's skew means neither alone works.
- Hybrid targets the outliers: Identify the small number of extreme accounts and route only those through the expensive path (merge at read) instead of penalizing everyone.
- Decouple posting from fan-out: A tweet is durable and visible to its author immediately; fan-out to followers happens asynchronously via a queue.
- Notifications aren't the same problem as timelines: Bounded-audience events (likes, replies) can afford simpler delivery than unbounded-audience events (a celebrity's tweet).
- Cache the hot path, accept eventual consistency: A timeline that's a few seconds stale is an acceptable tradeoff for sub-200ms reads at this scale.
Interview tip: Don't present fan-out-on-write and fan-out-on-read as a binary choice — name the hybrid model explicitly and explain what threshold triggers which path and why. Interviewers specifically probe the celebrity case because it's where naive designs collapse; walking through the read-time merge unprompted is a strong signal.
Follow-Up Questions to Consider
- How would you rank the timeline algorithmically instead of purely by recency?
- How would you handle a user who follows 10,000 accounts (following-count skew, not follower-count skew)?
- How would you support real-time timeline updates (new tweets appearing without a refresh)?
- How would you deduplicate retweets and quote-tweets in the timeline view?
- How would you handle deleting a tweet that's already been fanned out to millions of cached timelines?
Real-world trivia: Twitter's actual production system historically used a hybrid push/pull model much like the one described here, with precomputed timelines held in in-memory Redis clusters and a specific "celebrity" carve-out for high-follower accounts to avoid fan-out storms. Twitter also famously ran its early timeline and fan-out infrastructure on a service internally called "Timeline Service" backed by Redis, migrating over the years from a Ruby on Rails monolith toward a more service-oriented, largely Scala/JVM-based backend for exactly these scaling reasons.