Expert Case Studies

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.

August 14, 2026
case studytwittersocial feedfan-outfollower graphnotificationsscale

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

  1. Post a Tweet: Users compose tweets (text, media, up to a character limit)
  2. Follow/Unfollow: Users build a directed follower graph
  3. Home Timeline: See a reverse-chronological (or lightly ranked) feed of tweets from followed accounts
  4. Notifications: Get notified of likes, retweets, replies, and new followers
  5. Engagement: Like, retweet, reply
  6. Search (optional): Search tweets and users

Non-Functional Requirements

RequirementTarget
Scale100M+ tweets/day, 500M+ timeline reads/day
LatencyHome timeline loads in < 200ms (p99)
Availability99.99% uptime — feed is the core product surface
Follower skewMust handle accounts with 100M+ followers without degrading writes
ConsistencyEventual 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.

ModelMechanicsWrite CostRead CostBreaks Down When
Fan-out on write (push)On tweet, immediately write the tweet ID into every follower's precomputed timeline cacheO(followers) writes per tweetO(1) — just read the cacheA 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 flyO(1) — just append the tweet onceO(following count) reads per timeline loadA user following 5,000 accounts pays that cost on every timeline load
HybridPush for accounts below a follower threshold; skip push and merge at read time for accounts above itO(followers) only for normal accountsO(celebrities followed) extra merge workRarely — 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.

AspectRegular Account PathCelebrity Account Path
Write cost per tweetO(followers) — can be millionsO(1) — single write to tweet store
Read costO(1) cache lookupSmall merge: fetch N celebrity accounts' latest tweets
Staleness riskTimeline cache might lag by seconds under loadNone — always reads latest at request time
Who counts as a celebrityN/AFollower 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

sql
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).

sql
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:

sql
-- 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 DecisionRationale
Async delivery via queueEngagement actions (like, retweet) shouldn't block on notification delivery
Batching/deduplicationA viral tweet getting 10K likes/minute becomes one "10K people liked your tweet" notification, not 10K pushes
Bounded fan-out targetNotifications 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 timelineNotification read patterns (unread count, mark-as-read) differ from timeline patterns (infinite scroll by time)

Caching Strategy

CacheWhat It HoldsTTL / Eviction
Timeline cache (Redis sorted sets)Precomputed tweet IDs per user, from push fan-outTrimmed to last ~800 tweets per user; not time-based
Tweet content cacheHot tweet objects (text, counts) keyed by tweet_idLRU, short TTL, refreshed on engagement
Follower graph cacheFollower/following lists for high-degree accountsLRU — avoids repeated full graph scans during fan-out
Celebrity flag cacheWhether a user is above the fan-out thresholdRefreshed periodically, not per-tweet

Scaling Challenges & Solutions

ChallengeSolution
Celebrity write amplificationSkip fan-out above a follower threshold; merge celebrity tweets at read time
Timeline read latencyPrecomputed Redis sorted sets for the common case; small bounded merge for celebrities
Follower graph hot rowsCache high-degree follower/following lists; paginate fan-out in batches
Notification stormsBatch and deduplicate engagement notifications rather than one-per-event
Fan-out worker backlogKafka buffers events; workers scale horizontally and process in parallel batches
Timeline cache stalenessAcceptable eventual consistency — a few seconds of lag is invisible to users
Unbounded timeline growthTrim cached timelines to a fixed recent window (e.g., last 800 tweets)

Key Takeaways

  1. 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.
  2. 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.
  3. Decouple posting from fan-out: A tweet is durable and visible to its author immediately; fan-out to followers happens asynchronously via a queue.
  4. 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).
  5. 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

  1. How would you rank the timeline algorithmically instead of purely by recency?
  2. How would you handle a user who follows 10,000 accounts (following-count skew, not follower-count skew)?
  3. How would you support real-time timeline updates (new tweets appearing without a refresh)?
  4. How would you deduplicate retweets and quote-tweets in the timeline view?
  5. 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.