system-design50 min read

Design Twitter (X): The Complete System Design Guide — A Senior+ Guide | Ayodhyya

Design Twitter (X): The Complete System Design Guide

Building a real-time microblogging platform that handles 500M tweets/day, 330M users, and celebrity fanout at massive scale

Senior+ Guide 45+ min read 10,000+ words Ayodhyya

1. Introduction — Why Twitter is a Classic System Design Problem

Twitter, now rebranded as X, is one of the most popular microblogging platforms in the world. With over 330 million monthly active users, 500 million tweets posted every day, and peak traffic spikes that can reach 100 times the normal baseline during breaking news events, Twitter represents one of the most challenging distributed systems problems in production today. The platform must serve a real-time timeline to millions of users simultaneously, handle the fanout problem where a single tweet from a celebrity like Elon Musk with over 100 million followers must appear in millions of timelines within seconds, and provide full-text search across hundreds of billions of tweets.

The core challenge in designing Twitter is the intersection of a social graph with a real-time publishing system. Unlike a simple content delivery network where static content is cached and served, Twitter requires dynamic content that changes with every new tweet, every new follow, and every engagement event. The fanout problem is the defining architectural decision: when a user with millions of followers posts a tweet, should the system pre-compute the timeline for every follower (fanout-on-write), or should it merge tweets at query time when the user opens the app (fanout-on-read)? Twitter's production system uses a hybrid approach, and understanding why is the key insight of this system design exercise.

Interview Context: The Twitter system design question is frequently asked at senior and staff-level interviews at Google, Meta, Amazon, Microsoft, and top startups. It tests your understanding of social graph modeling, message queues, fanout strategies, caching, database sharding, and handling viral traffic spikes. A strong answer demonstrates your ability to reason about trade-offs between read-heavy and write-heavy paths in a distributed system.

This guide walks through every major component of Twitter's architecture, from the tweet posting pipeline to the timeline generation service, from the search index backed by Elasticsearch to the notification system that pushes alerts to mobile devices. We will cover C# code implementations, Mermaid architecture diagrams, database schemas, capacity estimates, and production-grade patterns for handling the hardest scalability challenges. By the end of this guide, you will have a complete mental model of how to design a Twitter-like system from scratch.

2. Functional and Non-Functional Requirements

Functional Requirements

#RequirementPriorityDetails
F1Post a tweetMustUser can post a tweet with up to 280 characters, images, videos, and polls
F2View home timelineMustUser sees a ranked feed of tweets from accounts they follow
F3View user timelineMustUser can view all tweets from a specific account in reverse chronological order
F4Follow / Unfollow usersMustUser can follow or unfollow other accounts
F5Like and retweetMustUser can like, retweet, and reply to tweets
F6Search tweetsMustFull-text search across tweets by keyword, hashtag, and mention
F7NotificationsShouldPush and in-app notifications for likes, retweets, follows, and mentions
F8Trending topicsShouldReal-time trending hashtags and topics based on volume and velocity
F9Direct messagesNicePrivate one-to-one and group messaging
F10Media uploadMustUpload and serve images, GIFs, and videos with CDN delivery

Non-Functional Requirements

RequirementTargetRationale
Availability99.99% (21.9 min downtime/year)Social media is a always-on platform; downtime during major events is unacceptable
Latency (timeline)p99 < 200msTimeline must load instantly to maintain user engagement
Latency (tweet post)p99 < 500msTweet posting should feel immediate with optimistic UI
ConsistencyEventual consistencyTimeline can be slightly stale; search index lags by 1-3 seconds
Throughput (reads)600K QPS400M DAU x 15 reads/day average across timeline, profiles, search
Throughput (writes)12K QPS average, 30K peak500M tweets/day = ~5,800 tweets/sec average with 5x peak
Durability99.999999%Tweets are permanent records; zero data loss is required
Storage182 TB/year for tweets500M tweets x 1KB average = 500GB/day raw data
Key Trade-off: Twitter prioritizes availability and low latency over strong consistency. Your home timeline may be 1-2 seconds behind reality, and that is an acceptable trade-off. If a tweet from a celebrity you follow does not appear for 2 seconds, the user experience is still acceptable. But if the timeline takes 2 seconds to load, users will churn.

3. Capacity Estimation and Back-of-Envelope Calculations

Write Path

With 500 million tweets per day, the average write throughput is 500,000,000 / 86,400 = approximately 5,787 tweets per second. During peak events such as the Super Bowl, World Cup, or breaking political news, this can spike to 15,000-30,000 tweets per second. Each tweet requires one write to the tweets database and one publish to the Kafka fanout topic. The fanout service then performs additional writes to timeline caches. For a user with 1,000 followers, each tweet generates 1,000 additional writes to Redis sorted sets. The total write amplification factor depends on the average follower count and the fanout threshold.

Read Path

With 400 million daily active users loading their timeline 5-10 times per day, we get 2 billion to 4 billion timeline reads per day. This translates to 23,000-46,000 reads per second on average, with peaks of 100,000-200,000 reads per second. Search queries add another 2 billion per day or approximately 23,000 QPS. Profile page views contribute another 10,000 QPS. The total read load is dominated by timeline reads, which is why caching and the fanout strategy are the most critical architectural decisions.

Storage Estimates

EntitySize per RecordDaily VolumeDaily StorageAnnual Storage
Tweets~1 KB (text metadata)500 million500 GB182 TB
Media (images)~300 KB average200 million60 TB22 PB
Media (videos)~5 MB average20 million100 TB36.5 PB
Follow relationships~50 bytes10 million changes500 MB182 GB
Timeline caches~8 bytes per tweet ID2 billion entries16 GB5.8 TB
Search index~2x tweet size500 million1 TB365 TB

Bandwidth Estimates

Outbound bandwidth for serving timelines: 600K QPS x 20 tweets per page x 1 KB per tweet = approximately 12 GB/s outbound. With CDN for media, the CDN handles 160 TB/day of media delivery which translates to approximately 1.85 GB/s average. Inbound bandwidth for tweet ingestion: 5,787 tweets/sec x 1 KB = approximately 5.7 MB/s for tweet text, plus media uploads at approximately 100 GB/s during peaks. The system is read-heavy with a read-to-write ratio of approximately 100:1.

Key Numbers Summary

graph LR A["500M tweets/day"] --> B["5.8K writes/sec avg"] B --> C["30K peak writes/sec"] D["400M DAU"] --> E["23K timeline reads/sec"] E --> F["200K peak reads/sec"] G["182 TB/year tweet storage"] --> H["365 TB/year search index"] I["12 GB/s outbound bandwidth"] --> J["1.85 GB/s CDN bandwidth"]

4. High-Level Architecture and Core Services

The Twitter system architecture consists of several core services that work together to handle the complete lifecycle of a tweet: from the moment a user types and posts a tweet, through the fanout pipeline that distributes it to followers' timelines, to the ranking engine that orders the timeline, and finally to the client that renders the feed. The architecture follows a service-oriented design with clear boundaries between the write path (tweet posting) and the read path (timeline generation, search, profile views).

graph TB Client["Mobile / Web Client"] --> LB["Load Balancer / API Gateway"] LB --> TweetService["Tweet Service"] LB --> TimelineService["Timeline Service"] LB --> SearchService["Search Service"] LB --> UserService["User Service"] LB --> NotificationService["Notification Service"] TweetService --> Kafka["Kafka Message Queue"] TweetService --> TweetDB["Tweet Database (Manhattan)"] Kafka --> FanoutService["Fanout Service"] FanoutService --> TimelineCache["Timeline Cache (Redis)"] FanoutService --> SearchIndex["Search Index (Elasticsearch)"] FanoutService --> NotificationService TimelineService --> TimelineCache TimelineService --> TweetCache["Tweet Cache (Redis)"] TimelineService --> CelebrityTimeline["Celebrity Timeline Service"] SearchService --> SearchIndex UserService --> GraphDB["Social Graph (FlockDB)"] UserService --> UserCache["User Cache (Redis)"] MediaService["Media Service"] --> CDN["CDN (CloudFront)"] MediaService --> ObjectStore["Object Storage (S3)"]

Core Services Breakdown

ServiceResponsibilityTechnologyScale
Tweet ServiceAccept new tweets, validate content, write to DB, publish to KafkaC# / .NET, gRPC6K-30K QPS writes
Timeline ServiceAssemble home feed by merging pre-computed and on-demand timelinesC# / .NET, Redis600K QPS reads
Fanout ServiceDistribute tweet IDs to followers' timeline cachesC# Workers, Kafka consumers1M+ fanout operations/sec
Search ServiceIndex tweets and serve search queriesElasticsearch cluster23K QPS search
User ServiceUser profiles, follow/unfollow, social graph queriesC# / .NET, FlockDB50K QPS reads
Notification ServicePush notifications, in-app alerts, email digestsC# / .NET, FCM, APNs100K notifications/sec
Media ServiceImage/video upload, transcoding, CDN deliveryC# / .NET, FFmpeg, S3100 GB/s peak
Architecture Principle: The write path (tweet posting) and read path (timeline serving) are completely decoupled via Kafka. This means a spike in tweet volume does not directly impact timeline read latency. The fanout service acts as the bridge, absorbing write bursts and pushing timeline updates asynchronously.

5. Data Model and Storage Schema

The data model for Twitter must support efficient lookups for several access patterns: fetching a user's tweets, fetching a user's timeline, checking follow relationships, and searching tweet content. The primary entities are User, Tweet, Follow, and Timeline. Each entity has different storage requirements and access patterns, which is why Twitter uses a polyglot persistence approach with different databases for different workloads.

// Twitter Data Model - PostgreSQL / Manhattan Schema
CREATE TABLE users (
    user_id       BIGSERIAL PRIMARY KEY,
    username      VARCHAR(15) UNIQUE NOT NULL,
    display_name  VARCHAR(50),
    bio           TEXT,
    profile_image_url VARCHAR(500),
    banner_image_url  VARCHAR(500),
    follower_count    INT DEFAULT 0,
    following_count   INT DEFAULT 0,
    tweet_count       INT DEFAULT 0,
    is_verified   BOOLEAN DEFAULT FALSE,
    is_celebrity  BOOLEAN DEFAULT FALSE,
    created_at    TIMESTAMP DEFAULT NOW(),
    updated_at    TIMESTAMP DEFAULT NOW()
);

CREATE TABLE tweets (
    tweet_id      BIGSERIAL PRIMARY KEY,
    user_id       BIGINT REFERENCES users(user_id),
    content       VARCHAR(280),
    media_urls    TEXT[],
    hashtags      TEXT[],
    mentions      BIGINT[],
    reply_to_tweet_id BIGINT,
    retweet_of_tweet_id BIGINT,
    like_count    INT DEFAULT 0,
    retweet_count INT DEFAULT 0,
    reply_count   INT DEFAULT 0,
    quote_count   INT DEFAULT 0,
    view_count    BIGINT DEFAULT 0,
    language      VARCHAR(10),
    is_sensitive  BOOLEAN DEFAULT FALSE,
    created_at    TIMESTAMP DEFAULT NOW()
);

CREATE TABLE follows (
    follower_id   BIGINT REFERENCES users(user_id),
    followee_id   BIGINT REFERENCES users(user_id),
    created_at    TIMESTAMP DEFAULT NOW(),
    PRIMARY KEY (follower_id, followee_id)
);

CREATE TABLE likes (
    user_id       BIGINT REFERENCES users(user_id),
    tweet_id      BIGINT REFERENCES tweets(tweet_id),
    created_at    TIMESTAMP DEFAULT NOW(),
    PRIMARY KEY (user_id, tweet_id)
);

CREATE TABLE retweets (
    user_id       BIGINT REFERENCES users(user_id),
    tweet_id      BIGINT REFERENCES tweets(tweet_id),
    created_at    TIMESTAMP DEFAULT NOW(),
    PRIMARY KEY (user_id, tweet_id)
);

CREATE INDEX idx_tweets_user_created ON tweets(user_id, created_at DESC);
CREATE INDEX idx_tweets_created ON tweets(created_at DESC);
CREATE INDEX idx_follows_follower ON follows(follower_id);
CREATE INDEX idx_follows_followee ON follows(followee_id);

Storage Technology Choices

DataPrimary StoreCache LayerAccess Pattern
TweetsManhattan (distributed KV)Redis (hot tweets)Read-heavy, append-only writes
User profilesManhattanRedisRead-heavy, infrequent updates
Follow graphFlockDB (graph)Redis setsRead-heavy, adjacency list queries
Timeline cachesRedis sorted setsN/A (is the cache)Write-heavy on fanout, read-heavy on timeline
Search indexElasticsearch (Earlybird)N/AWrite-once, read-heavy search
Likes / RetweetsManhattanRedisWrite-once, read for counts and checks

Why Manhattan Over Traditional RDBMS?

Manhattan is Twitter's custom distributed key-value store that provides multi-dimensional indexing, automatic sharding across data centers, and tunable consistency. Traditional RDBMS systems like PostgreSQL struggle at Twitter's scale because a single table with hundreds of billions of rows cannot be efficiently sharded with range-based partitioning. Manhattan stores data across thousands of machines with consistent hashing on the tweet_id, ensuring均匀 distribution. It also provides point-in-time recovery and replication across three data centers for durability.

6. Tweet Posting Pipeline and Write Path

When a user taps the tweet button, the client sends a POST request to the Tweet Service. The write path involves several steps: content validation, media processing, database write, Kafka publish, and fanout triggering. The critical design goal is to return success to the user as fast as possible (under 500ms) while ensuring the tweet eventually appears in all relevant timelines. This is achieved through optimistic writes and asynchronous fanout processing.

// Tweet Posting Service - C# Implementation
public class TweetService
{
    private readonly ITweetRepository _tweetRepo;
    private readonly IKafkaProducer _kafkaProducer;
    private readonly IContentValidator _contentValidator;
    private readonly IMediaService _mediaService;

    public async Task<TweetResponse> PostTweetAsync(PostTweetRequest request)
    {
        // Step 1: Validate content
        var validation = _contentValidator.Validate(request.Content);
        if (!validation.IsValid)
            throw new ValidationException(validation.Errors);

        // Step 2: Process media attachments
        var mediaUrls = new List<string>();
        if (request.MediaIds?.Any() == true)
        {
            mediaUrls = await _mediaService.ProcessMediaAsync(request.MediaIds);
        }

        // Step 3: Extract hashtags and mentions
        var hashtags = ExtractHashtags(request.Content);
        var mentions = ExtractMentions(request.Content);

        // Step 4: Create tweet entity
        var tweet = new Tweet
        {
            TweetId = SnowflakeIdGenerator.NextId(),
            UserId = request.UserId,
            Content = SanitizeContent(request.Content),
            MediaUrls = mediaUrls.ToArray(),
            Hashtags = hashtags.ToArray(),
            Mentions = mentions.ToArray(),
            ReplyToTweetId = request.ReplyToTweetId,
            RetweetOfTweetId = request.RetweetOfTweetId,
            Language = DetectLanguage(request.Content),
            CreatedAt = DateTime.UtcNow
        };

        // Step 5: Write to database (durable storage)
        await _tweetRepo.InsertAsync(tweet);

        // Step 6: Publish to Kafka for async fanout
        await _kafkaProducer.ProduceAsync("tweet-events", new TweetEvent
        {
            TweetId = tweet.TweetId,
            UserId = tweet.UserId,
            EventType = tweet.RetweetOfTweetId.HasValue
                ? TweetEventType.Retweet
                : TweetEventType.NewTweet,
            Timestamp = tweet.CreatedAt,
            FollowerCount = await GetFollowerCountAsync(tweet.UserId)
        });

        // Step 7: Index in search (async via Kafka consumer)
        // Step 8: Update user tweet count (async)

        return new TweetResponse
        {
            TweetId = tweet.TweetId,
            CreatedAt = tweet.CreatedAt,
            Status = "posted"
        };
    }

    private List<string> ExtractHashtags(string content)
    {
        var matches = Regex.Matches(content, @"#(\w+)");
        return matches.Select(m => m.Groups[1].Value.ToLower()).ToList();
    }

    private List<long> ExtractMentions(string content)
    {
        var matches = Regex.Matches(content, @"@(\w+)");
        return matches.Select(m => ResolveUsername(m.Groups[1].Value))
                      .Where(id => id.HasValue)
                      .Select(id => id.Value).ToList();
    }
}

Kafka Topic Design for Tweet Events

// Kafka topic configuration for tweet events
// Topic: tweet-events
// Partitions: 128 (one per million follower range)
// Replication factor: 3
// Retention: 7 days

public class TweetEvent
{
    public long TweetId { get; set; }
    public long UserId { get; set; }
    public TweetEventType EventType { get; set; }
    public DateTime Timestamp { get; set; }
    public int FollowerCount { get; set; }
}

public enum TweetEventType
{
    NewTweet = 1,
    Reply = 2,
    Retweet = 3,
    Quote = 4,
    Delete = 5
}

// Kafka partitioning strategy: partition by user_id % 128
// This ensures all events from the same user go to the same partition
// maintaining ordering per user
Design Decision: The tweet ID uses a Snowflake-like ID generator that encodes timestamp, machine ID, and sequence number into a 64-bit integer. This provides globally unique, roughly time-ordered IDs without requiring a central authority. The timestamp component allows efficient range queries and time-based partitioning.

7. Fanout-on-Write vs Fanout-on-Read

The fanout strategy is the single most important architectural decision in Twitter's design. Fanout is the process of distributing a tweet from the author to all relevant followers' timelines. There are two fundamental approaches, and Twitter uses a hybrid of both.

Fanout-on-Write (Push Model)

When a user posts a tweet, the fanout service immediately inserts the tweet ID into the timeline cache (a Redis sorted set) of every follower. When a follower opens the app, their timeline is already pre-computed and ready to serve. The advantage is extremely fast timeline reads (O(1) lookup in Redis). The disadvantage is write amplification: a user with 100,000 followers generates 100,000 write operations per tweet. For users with millions of followers, this becomes prohibitively expensive.

Fanout-on-Read (Pull Model)

Tweets are stored in the author's profile, and when a user opens their timeline, the system fetches recent tweets from all followed accounts and merges them. The advantage is zero write amplification: posting a tweet is always a single write. The disadvantage is high read latency: for a user following 500 accounts, the timeline service must fetch from 500 different sources and merge. This becomes even worse if some of those accounts are celebrities with thousands of recent tweets.

Twitter's Hybrid Approach

Twitter uses fanout-on-write for regular users with fewer than 5,000 followers, and fanout-on-read for celebrity accounts with more than 5,000 followers. The threshold of 5,000 is chosen based on the analysis that the top 1% of accounts (the celebrities) account for the majority of timeline traffic but are expensive to push to all followers. By using pull for celebrities, the system avoids millions of unnecessary writes per celebrity tweet while keeping the timeline fast for the 99% of users who follow mostly regular accounts.

// Hybrid Fanout Service - C# Implementation
public class FanoutService
{
    private readonly IRedisCluster _redis;
    private readonly IFollowerStore _followerStore;
    private readonly ITweetCache _tweetCache;

    private const int CelebrityThreshold = 5000;

    public async Task FanoutTweetAsync(TweetEvent tweetEvent)
    {
        var followerCount = tweetEvent.FollowerCount;

        if (followerCount <= CelebrityThreshold)
        {
            // Fanout-on-Write: Push to all followers' timelines
            await PushToFollowersAsync(tweetEvent);
        }
        else
        {
            // Fanout-on-Read: Store in celebrity tweet cache
            await StoreInCelebrityCacheAsync(tweetEvent);
        }

        // Always index in search regardless of fanout strategy
        await IndexTweetForSearchAsync(tweetEvent);
    }

    private async Task PushToFollowersAsync(TweetEvent tweetEvent)
    {
        var followerIds = await _followerStore.GetFollowerIdsAsync(
            tweetEvent.UserId, limit: 10000);

        // Batch the writes using Redis pipeline
        var batchSize = 100;
        for (int i = 0; i < followerIds.Count; i += batchSize)
        {
            var batch = followerIds.Skip(i).Take(batchSize).ToList();
            var pipeline = _redis.CreatePipeline();

            foreach (var followerId in batch)
            {
                // Add tweet to follower's timeline sorted set
                pipeline.ZAdd(
                    $"timeline:{followerId}",
                    tweetEvent.Timestamp.Ticks,
                    tweetEvent.TweetId.ToString());

                // Trim timeline to most recent 800 tweets
                pipeline.ZRemRangeByRank(
                    $"timeline:{followerId}", 0, -801);

                // Increment unread count
                pipeline.HIncrBy(
                    $"user:{followerId}:unread", "timeline", 1);
            }

            await pipeline.ExecuteAsync();
        }
    }

    private async Task StoreInCelebrityCacheAsync(TweetEvent tweetEvent)
    {
        // Store in a separate Redis list for celebrity tweets
        var key = $"celebrity_tweets:{tweetEvent.UserId}";
        var tweetData = JsonSerializer.Serialize(new
        {
            TweetId = tweetEvent.TweetId,
            Timestamp = tweetEvent.Timestamp
        });

        await _redis.LPushAsync(key, tweetData);
        await _redis.LTrimAsync(key, 0, 199); // Keep last 200 tweets
        await _redis.ExpireAsync(key, TimeSpan.FromHours(24));

        // Mark this user as a celebrity for timeline assembly
        var celebKey = $"celebrity_follows:{tweetEvent.UserId}";
        // Followers will check this set during timeline generation
    }
}

Fanout Strategy Comparison

AspectFanout-on-WriteFanout-on-ReadHybrid (Twitter)
Write cost per tweetO(followers) - highO(1) - constantO(followers) if small, O(1) if celebrity
Read cost per timelineO(1) - pre-computedO(followed accounts) - highO(1) + O(celebrity followed)
Timeline freshnessImmediateImmediateImmediate for small, 1-2s delay for celebrity
Storage costHigh (N timeline copies)Low (only source tweets)Medium (push for regular, pull for celebrity)
Celebrity tweet costProhibitive (millions of writes)CheapCheap (uses pull path)
ImplementationSimpleComplex merge logicMedium complexity
Celebrity Problem: When Elon Musk (100M+ followers) tweets, fanout-on-write would require 100 million Redis ZADD operations. At 100K writes/sec per Redis instance, this would take 1,000 seconds (over 16 minutes). With the hybrid approach, the tweet is stored once in the celebrity cache, and each follower merges it on read in O(1) time.

8. Timeline Generation and Home Feed

Timeline generation is the process of assembling a user's home feed when they open the Twitter app. The timeline service must merge tweets from two sources: the pre-computed timeline (from fanout-on-write) for regular followed accounts, and the on-demand celebrity tweets (from fanout-on-read) for celebrity accounts. The merged timeline is then ranked, hydrated with full tweet content, and paginated for delivery to the client.

sequenceDiagram participant Client participant TimelineService participant Redis participant TweetCache participant CelebrityService Client->>TimelineService: GET /api/timeline?cursor=123&limit=20 TimelineService->>Redis: ZREVRANGEBYSCORE timeline:userId (cursor) -inf LIMIT 0 200 Redis-->>TimelineService: List of tweet IDs (pre-computed) TimelineService->>TweetCache: MGET tweet:1001 tweet:1002 ... TweetCache-->>TimelineService: Full tweet objects TimelineService->>CelebrityService: Get celebrity tweets for followed celebs CelebrityService->>Redis: LRANGE celebrity_tweets:celebId 0 9 Redis-->>CelebrityService: Celebrity tweet IDs CelebrityService-->>TimelineService: Celebrity tweets TimelineService->>TimelineService: Merge + Rank + Deduplicate TimelineService->>TimelineService: Slice top 20 results TimelineService-->>Client: Paginated timeline response
// Timeline Generation Service - C# Implementation
public class TimelineService
{
    private readonly IRedisCluster _redis;
    private readonly ITweetCache _tweetCache;
    private readonly ICelebrityService _celebrityService;
    private readonly IRankingService _rankingService;

    public async Task<TimelineResponse> GetHomeTimelineAsync(
        long userId, long? cursor, int limit = 20)
    {
        var startTime = DateTime.UtcNow;

        // Step 1: Fetch pre-computed timeline from Redis
        // (tweets pushed by fanout-on-write from regular followed accounts)
        var maxScore = cursor.HasValue
            ? cursor.Value.ToString()
            : "+inf";

        var tweetIds = await _redis.ZRevRangeByScoreAsync(
            $"timeline:{userId}",
            maxScore,
            "-inf",
            skip: 0,
            take: 200); // Fetch 200 to have enough after ranking

        // Step 2: Hydrate tweet objects from cache
        var tweets = new List<Tweet>();
        foreach (var tweetId in tweetIds)
        {
            var tweet = await _tweetCache.GetTweetAsync(long.Parse(tweetId));
            if (tweet != null)
                tweets.Add(tweet);
        }

        // Step 3: Fetch celebrity tweets (fanout-on-read path)
        var celebrityIds = await _celebrityService
            .GetCelebrityFolloweesAsync(userId);

        foreach (var celebId in celebrityIds)
        {
            var celebTweetIds = await _redis.LRangeAsync(
                $"celebrity_tweets:{celebId}", 0, 9);

            foreach (var celebTweetId in celebTweetIds)
            {
                var celebTweet = await _tweetCache.GetTweetAsync(
                    long.Parse(celebTweetId));
                if (celebTweet != null)
                    tweets.Add(celebTweet);
            }
        }

        // Step 4: Remove duplicates (user might see tweet from both paths)
        tweets = tweets.DistinctBy(t => t.TweetId).ToList();

        // Step 5: Remove tweets the user has already seen
        var lastSeenId = await _redis.GetAsync(
            $"user:{userId}:last_seen_timeline_id");
        if (lastSeenId != null)
        {
            tweets = tweets.Where(t =>
                t.TweetId > long.Parse(lastSeenId)).ToList();
        }

        // Step 6: Rank the tweets
        var viewerContext = await BuildViewerContextAsync(userId);
        var rankedTweets = tweets
            .Select(t => new { Tweet = t, Score = _rankingService.RankTweet(t, viewerContext) })
            .OrderByDescending(x => x.Score)
            .Take(limit)
            .Select(x => x.Tweet)
            .ToList();

        // Step 7: Build cursor for next page
        var nextCursor = rankedTweets.Any()
            ? rankedTweets.Last().TweetId
            : (long?)null;

        // Step 8: Update last seen timestamp
        await _redis.SetAsync(
            $"user:{userId}:last_seen_timeline_id",
            rankedTweets.FirstOrDefault()?.TweetId.ToString(),
            TimeSpan.FromDays(7));

        return new TimelineResponse
        {
            Tweets = rankedTweets,
            NextCursor = nextCursor,
            HasMore = tweetIds.Count == 200,
            LatencyMs = (DateTime.UtcNow - startTime).TotalMilliseconds
        };
    }
}

User Timeline vs Home Timeline

AspectHome TimelineUser Timeline
Data sourceMerged from all followed accounts + celebrity cacheDirect query on user's own tweets
RankingML-ranked by engagement, recency, affinityReverse chronological (no ranking)
Cache layerRedis sorted set + celebrity Redis listRedis sorted set per user
Read throughput200K QPS peak50K QPS peak
Fanout requiredYes (for non-celebrity follows)No (direct query)

9. Timeline Ranking and Relevance Algorithm

The naive reverse-chronological timeline shows tweets in the order they were posted. While simple, this approach misses important tweets that the user would find engaging. Twitter's ranking algorithm (internally known as Earlybird for search and Home Mixer for timeline) uses a multi-signal scoring model to reorder tweets based on predicted engagement. The ranking model considers recency, engagement velocity, author affinity, content type, and language relevance.

The ranking pipeline works in two phases. First, a lightweight pre-ranking model scores the top 200 candidate tweets from the cache. This pre-ranking uses a simple linear model that can be computed in under 10ms. Second, a heavier ML model re-ranks the top 50 candidates using more expensive features like user engagement history, tweet virality prediction, and social graph distance. The final ranked list is what the user sees on their home timeline.

// Timeline Ranking Service - C# Implementation
public class RankingService
{
    private readonly IEngagementStore _engagementStore;
    private readonly IAffinityService _affinityService;

    public double RankTweet(Tweet tweet, ViewerContext context)
    {
        // Signal 1: Recency (exponential decay over 24 hours)
        var hoursSincePosted = (DateTime.UtcNow - tweet.CreatedAt).TotalHours;
        var recencyScore = Math.Exp(-0.1 * hoursSincePosted);

        // Signal 2: Engagement velocity (engagements per hour)
        var ageHours = Math.Max(1, hoursSincePosted);
        var totalEngagements = tweet.LikeCount
            + tweet.RetweetCount * 2
            + tweet.ReplyCount * 3
            + tweet.QuoteCount * 2;
        var velocityScore = Math.Log10(totalEngagements + 1) / Math.Log10(ageHours + 1);

        // Signal 3: Author affinity (how much user interacts with this author)
        var affinityScore = _affinityService.GetAffinityScore(
            context.UserId, tweet.UserId);

        // Signal 4: Content type bonus (media-rich tweets get boost)
        var mediaBoost = tweet.MediaUrls?.Length > 0 ? 1.5 : 0;

        // Signal 5: Language relevance
        var languageMatch = tweet.Language == context.PreferredLanguage ? 1.0 : 0.3;

        // Signal 6: Is this a reply? Replies get lower weight
        var replyPenalty = tweet.ReplyToTweetId.HasValue ? 0.4 : 1.0;

        // Signal 7: Author verification bonus
        var verificationBonus = tweet.IsAuthorVerified ? 1.2 : 1.0;

        // Weighted combination
        var finalScore =
            recencyScore * 0.30 +
            velocityScore * 0.25 +
            affinityScore * 0.20 +
            mediaBoost * 0.08 +
            languageMatch * 0.07 +
            replyPenalty * 0.05 +
            verificationBonus * 0.05;

        return finalScore;
    }

    public List<Tweet> RerankWithMLModel(
        List<Tweet> candidates, ViewerContext context)
    {
        // Phase 2: Heavy ML re-ranking on top 50 candidates
        // This calls a trained model (e.g., XGBoost or neural network)
        // that considers deeper features:
        // - User's historical engagement with similar content
        // - Tweet virality prediction (will this tweet get 10x more engagement?)
        // - Social proof (have the user's friends engaged with this tweet?)
        // - Temporal patterns (does user usually engage at this time of day?)

        var features = candidates.Select(tweet =>
            ExtractFeatures(tweet, context)).ToArray();

        var predictions = _mlModel.PredictBatch(features);

        return candidates
            .Zip(predictions, (tweet, score) => new { Tweet = tweet, Score = score })
            .OrderByDescending(x => x.Score)
            .Select(x => x.Tweet)
            .Take(20)
            .ToList();
    }

    private float[] ExtractFeatures(Tweet tweet, ViewerContext context)
    {
        return new float[]
        {
            (float)(DateTime.UtcNow - tweet.CreatedAt).TotalHours,
            tweet.LikeCount,
            tweet.RetweetCount,
            tweet.ReplyCount,
            tweet.MediaUrls?.Length ?? 0,
            tweet.Content.Length,
            _affinityService.GetAffinityScore(context.UserId, tweet.UserId),
            context.FollowerCount,
            tweet.IsAuthorVerified ? 1f : 0f,
            tweet.Language == context.PreferredLanguage ? 1f : 0f
        };
    }
}

Ranking Signal Weights

SignalWeightWhy It Matters
Recency0.30Users expect fresh content; tweets older than 24 hours get exponentially penalized
Engagement velocity0.25Tweets gaining rapid engagement indicate viral or high-quality content
Author affinity0.20Users care more about tweets from accounts they frequently interact with
Media content0.08Tweets with images and videos get 2-3x more engagement on average
Language match0.07Users prefer content in their preferred language
Reply penalty0.05Replies to other tweets are less interesting as standalone timeline content
Verification bonus0.05Verified accounts tend to produce higher quality content

10. Real-Time Search with Elasticsearch

Twitter search handles over 2 billion queries per day, making it one of the largest Elasticsearch deployments in the world. The search system, internally known as Earlybird, provides full-text search across hundreds of billions of tweets with near-real-time indexing (1-3 seconds from tweet post to search availability). The system must handle a wide variety of query types: keyword search, hashtag lookup, mention filtering, user-specific search, and advanced query operators like exclusion and phrase matching.

The search architecture uses a custom Elasticsearch cluster with several optimizations. The index is sharded across hundreds of nodes with a custom routing strategy that ensures tweets from the same time period are co-located on the same shards. This allows the search system to efficiently handle time-range queries, which are the most common search pattern. The index uses a two-tier structure: a small in-memory segment for the most recent tweets (last 24 hours) and a larger on-disk segment for older tweets.

// Elasticsearch Index Mapping for Tweets
PUT /tweets_v2
{
  "settings": {
    "number_of_shards": 48,
    "number_of_replicas": 2,
    "refresh_interval": "1s",
    "index.routing.partition_size": 6
  },
  "mappings": {
    "properties": {
      "tweet_id":       { "type": "long" },
      "user_id":        { "type": "long" },
      "content":        { "type": "text", "analyzer": "english",
                         "fields": {
                           "keyword": { "type": "keyword" }
                         }
                       },
      "hashtags":       { "type": "keyword" },
      "mentions":       { "type": "long" },
      "language":       { "type": "keyword" },
      "like_count":     { "type": "integer" },
      "retweet_count":  { "type": "integer" },
      "reply_count":    { "type": "integer" },
      "view_count":     { "type": "long" },
      "has_media":      { "type": "boolean" },
      "is_reply":       { "type": "boolean" },
      "is_retweet":     { "type": "boolean" },
      "created_at":     { "type": "date" },
      "author_verified":{ "type": "boolean" }
    }
  }
}
// Search Service - C# Implementation
public class SearchService
{
    private readonly IElasticClient _elastic;

    public async Task<SearchResponse> SearchTweetsAsync(SearchRequest request)
    {
        var searchDescriptor = new SearchDescriptor<TweetDocument>()
            .Index("tweets_v2")
            .Size(request.Limit ?? 20)
            .From(request.Offset ?? 0)
            .Query(q =>
            {
                var queries = new List<IQueryContainer>();

                // Parse query type
                if (request.Query.StartsWith("#"))
                {
                    // Hashtag search
                    queries.Add(q.Term(t =>
                        t.Field(f => f.Hashtags)
                         .Value(request.Query.Substring(1).ToLower())));
                }
                else if (request.Query.StartsWith("@"))
                {
                    // Mention search
                    var userId = await ResolveUsername(request.Query.Substring(1));
                    queries.Add(q.Term(t =>
                        t.Field(f => f.UserId).Value(userId)));
                }
                else
                {
                    // Full-text search with BM25
                    queries.Add(q.MultiMatch(mm =>
                        mm.Fields(f =>
                            f.Field(ff => ff.Content, 2.0)
                             .Field(ff => ff.Hashtags, 1.5))
                         .Query(request.Query)
                         .Type(TextQueryType.BestFields)
                         .Fuzziness(Fuzziness.Auto)));
                }

                // Time range filter
                if (request.Since.HasValue)
                {
                    queries.Add(q.DateRange(dr =>
                        dr.Field(f => f.CreatedAt)
                         .GreaterThanOrEquals(request.Since.Value)));
                }

                // Language filter
                if (!string.IsNullOrEmpty(request.Language))
                {
                    queries.Add(q.Term(t =>
                        t.Field(f => f.Language)
                         .Value(request.Language)));
                }

                // Exclude retweets if requested
                if (request.ExcludeRetweets)
                {
                    queries.Add(q.Term(t =>
                        t.Field(f => f.IsRetweet).Value(false)));
                }

                return q.Bool(b => b.Must(queries.ToArray()));
            })
            .Sort(s =>
            {
                // Sort by relevance score (BM25) + engagement boost
                if (request.SortBy == "engagement")
                    return s.Descending(f =>
                        Field("like_count") + Field("retweet_count") * 2);
                return s.Descending(SortSpecialField.Score);
            })
            .Highlight(h =>
                h.PreTags("<mark>")
                 .PostTags("</mark>")
                 .Fields(
                     f => f.Field(ff => ff.Content)
                             .FragmentSize(150)
                             .NumberOfFragments(1)));

        var response = await _elastic.SearchAsync<TweetDocument>(searchDescriptor);

        return new SearchResponse
        {
            Tweets = response.Documents.ToList(),
            TotalHits = response.Total,
            took = response.TookMilliseconds,
            Highlights = response.Hits.Select(h => h.Highlight).ToList()
        };
    }
}

Search Architecture Components

ComponentTechnologyRole
Real-time indexIn-memory segment (custom)Indexes tweets within 1-3 seconds for latest 24 hours
Search indexElasticsearch cluster (48 shards)Full-text search across all historical tweets
AutocompleteTrie-based serviceSuggests hashtags, usernames, and trending queries
Spelling correctionEdit-distance modelCorrects typos in search queries
Anti-spam filterML classifierRemoves spam and low-quality tweets from search results

11. Social Graph Storage and Follow Relationships

The social graph is the foundation of Twitter's architecture. Every feature — from the home timeline to notifications to search — depends on knowing who follows whom. At Twitter's scale, the social graph contains over 100 billion follow relationships stored across thousands of machines. The primary access patterns are: given a user, return their followers (fanout-on-write needs this), given a user, return who they follow (timeline generation needs this), and given two users, check if one follows the other (authorization checks).

Twitter uses FlockDB, a custom graph database designed for shallow, wide graph traversals. Unlike Neo4j or other graph databases that excel at deep traversals (friend-of-friend recommendations), FlockDB is optimized for the specific pattern of "get all edges incident to this node." The data model is simple: a edges table with columns (source_id, destination_id, status, created_at), sharded by source_id. This ensures that all follow relationships for a single user are co-located on the same shard, enabling efficient range scans.

// Social Graph Service - C# Implementation
public class SocialGraphService
{
    private readonly IFlockDBClient _flockDb;
    private readonly IRedisCluster _redis;

    public async Task FollowUserAsync(long followerId, long followeeId)
    {
        // Validate both users exist
        var users = await _userService.GetUsersAsync(
            new[] { followerId, followeeId });
        if (users.Count != 2)
            throw new NotFoundException("User not found");

        // Write to FlockDB (primary store)
        await _flockDb.InsertEdgeAsync(new GraphEdge
        {
            SourceId = followerId,
            DestinationId = followeeId,
            Status = EdgeStatus.Active,
            CreatedAt = DateTime.UtcNow
        });

        // Update follower/following counts (async)
        await _eventBus.PublishAsync(new FollowEvent
        {
            FollowerId = followerId,
            FolloweeId = followeeId,
            Action = FollowAction.Follow
        });

        // Invalidate caches
        await _redis.SetRemoveAsync(
            $"followers:{followeeId}:set", followerId.ToString());
        await _redis.SetAddAsync(
            $"following:{followerId}:set", followeeId.ToString());

        // Update fanout strategy if needed
        var followerCount = await GetFollowerCountAsync(followeeId);
        if (followerCount >= FanoutService.CelebrityThreshold)
        {
            await _redis.SetAddAsync("celebrity_users", followeeId.ToString());
        }
    }

    public async Task<List<long>> GetFollowersAsync(
        long userId, int offset = 0, int limit = 100)
    {
        // Try cache first
        var cached = await _redis.ZRangeAsync(
            $"followers:{userId}", offset, offset + limit - 1);
        if (cached.Length == limit)
            return cached.Select(long.Parse).ToList();

        // Fall back to FlockDB
        var followers = await _flockDb.GetEdgesAsync(
            sourceId: null,
            destinationId: userId,
            status: EdgeStatus.Active,
            offset: offset,
            limit: limit);

        return followers.Select(e => e.SourceId).ToList();
    }

    public async Task<bool> IsFollowingAsync(long followerId, long followeeId)
    {
        // Check Redis set for fast lookup
        return await _redis.SetContainsAsync(
            $"following:{followerId}:set", followeeId.ToString());
    }

    public async Task<int> GetFollowerCountAsync(long userId)
    {
        var count = await _redis.GetLongAsync($"user:{userId}:follower_count");
        if (count.HasValue) return (int)count.Value;

        // Recompute from FlockDB if cache miss
        var countValue = await _flockDb.CountEdgesAsync(
            sourceId: null,
            destinationId: userId,
            status: EdgeStatus.Active);

        await _redis.SetLongAsync(
            $"user:{userId}:follower_count", countValue, TimeSpan.FromMinutes(5));
        return (int)countValue;
    }
}

FlockDB Sharding Strategy

graph TB subgraph "FlockDB Shard Layout" S1["Shard 1: users 1-10M"] --> T1["Edges table (source_id sharded)"] S2["Shard 2: users 10M-20M"] --> T2["Edges table"] S3["Shard 3: users 20M-30M"] --> T3["Edges table"] SN["Shard N: ..."] --> TN["Edges table"] end FlockDB --> MySQL["MySQL (per shard)"] FlockDB --> Cache["Redis cache layer"]

FlockDB shards edges by source_id (the follower), meaning all of a user's follow relationships are stored together. This is optimal for the "get who I follow" query pattern needed by timeline generation. For the reverse query "get my followers" (needed by fanout), FlockDB maintains a secondary index on destination_id. The MySQL backing store provides durability while Redis provides the hot cache layer for frequently accessed adjacency lists.

12. Handling Viral Events and Thundering Herds

Viral events are the ultimate stress test for Twitter's architecture. When a major event occurs — a celebrity death, a World Cup final, an election result — millions of users flood the platform simultaneously. Traffic can spike 100x within minutes. The system must handle this gracefully without cascading failures. Twitter has experienced several high-profile outages during viral events, which drove the development of several resilience patterns.

The thundering herd problem occurs when a popular tweet triggers millions of timeline reads simultaneously, overwhelming the cache and database layers. The first defense is request coalescing: when 10,000 concurrent requests arrive for the same celebrity's timeline, only one backend fetch is made, and the result is shared across all waiting requests. The second defense is stale-while-revalidate: if the cache is being rebuilt, serve the stale version with a header indicating it may be slightly outdated. The third defense is progressive degradation: during extreme load, the system can reduce the number of tweets per timeline page, disable ranking, or serve a simplified chronological feed.

// Request Coalescing and Circuit Breaker - C# Implementation
public class CoalescedTimelineFetcher
{
    private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new();
    private readonly ConcurrentDictionary<string, Task<TimelineResult>> _inflight = new();
    private readonly ICircuitBreaker _circuitBreaker;

    public async Task<TimelineResult> FetchWithCoalescingAsync(
        long userId, long? celebId)
    {
        var key = $"timeline:{userId}:{celebId}";

        // Check if there is already an in-flight request for this key
        if (_inflight.TryGetValue(key, out var existingTask))
        {
            // Coalesce: wait for the existing request to complete
            return await existingTask;
        }

        // Create a new request with coalescing
        var tcs = new TaskCompletionSource<TimelineResult>();
        if (_inflight.TryAdd(key, tcs.Task))
        {
            try
            {
                // Use circuit breaker to prevent cascading failures
                var result = await _circuitBreaker.ExecuteAsync(async () =>
                {
                    return await FetchTimelineFromBackend(userId, celebId);
                });

                tcs.SetResult(result);
                return result;
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);

                // Serve stale data if available
                var staleResult = await GetStaleTimelineAsync(userId, celebId);
                if (staleResult != null)
                {
                    staleResult.IsStale = true;
                    return staleResult;
                }
                throw;
            }
            finally
            {
                _inflight.TryRemove(key, out _);
            }
        }

        return await _inflight[key];
    }

    private async Task<TimelineResult?> GetStaleTimelineAsync(
        long userId, long? celebId)
    {
        // Try to serve stale data from a secondary cache
        var staleKey = $"timeline_stale:{userId}:{celebId}";
        var staleData = await _redis.GetAsync(staleKey);
        if (staleData != null)
        {
            return JsonSerializer.Deserialize<TimelineResult>(staleData);
        }
        return null;
    }
}

// Circuit Breaker configuration
public class TwitterCircuitBreaker : ICircuitBreaker
{
    private int _failureCount = 0;
    private DateTime _lastFailure = DateTime.MinValue;
    private const int FailureThreshold = 5;
    private const int RecoverySeconds = 30;

    public async Task<T> ExecuteAsync<T>(Func<Task<T>> action)
    {
        if (_failureCount >= FailureThreshold)
        {
            if ((DateTime.UtcNow - _lastFailure).TotalSeconds < RecoverySeconds)
                throw new CircuitBreakerOpenException(
                    "Circuit breaker is open. Serving stale data.");
            _failureCount = 0; // Try half-open
        }

        try
        {
            var result = await action();
            _failureCount = 0;
            return result;
        }
        catch (Exception)
        {
            _failureCount++;
            _lastFailure = DateTime.UtcNow;
            throw;
        }
    }
}

Resilience Patterns Summary

PatternPurposeImplementation
Request coalescingPrevent duplicate backend calls for same dataConcurrentDictionary of in-flight tasks
Circuit breakerStop calling failing backends, serve stale dataFailure count threshold + recovery timer
Stale-while-revalidateServe slightly outdated data during cache rebuildSecondary Redis key with TTL
Rate limitingProtect backends from excessive requestsToken bucket per user per endpoint
Load sheddingDrop non-critical requests during extreme loadPriority queue with low-priority drop
Graceful degradationReduce feature richness to maintain availabilitySimplified timeline without ranking
Retry with backoffHandle transient failures without thundering retryExponential backoff with jitter

13. Notification System

Twitter's notification system must deliver millions of notifications per minute across push notifications (iOS/Android), in-app notification center, email digests, and web browser push. The system must handle notification storms during viral events (a celebrity tweet can trigger millions of like notifications within minutes) while respecting user preferences for notification frequency and channels. The core challenge is volume: with 330 million users generating hundreds of millions of engagement events daily, the notification system processes over 100 billion notifications per day.

The notification architecture uses a fan-in, fan-out pattern. Engagement events (likes, retweets, follows, mentions) are published to a Kafka topic. The Notification Service consumes these events, determines the target user, checks notification preferences, deduplicates, batches, and routes to the appropriate delivery channel. For high-volume events (like a viral tweet getting millions of likes), the system aggregates notifications: instead of sending "User A liked your tweet" and "User B liked your tweet" separately, it sends "User A and 45,000 others liked your tweet."

// Notification Service - C# Implementation
public class NotificationService
{
    private readonly IKafkaConsumer _consumer;
    private readonly IPushService _pushService;
    private readonly IEmailService _emailService;
    private readonly INotificationStore _notificationStore;
    private readonly INotificationPreferences _preferences;

    public async Task ProcessEngagementEventAsync(EngagementEvent evt)
    {
        // Step 1: Determine target user (tweet author)
        var targetUserId = await GetTweetAuthorAsync(evt.TweetId);
        if (targetUserId == evt.ActorUserId) return; // No self-notifications

        // Step 2: Check notification preferences
        var prefs = await _preferences.GetAsync(targetUserId);
        if (!IsNotificationEnabled(evt.Type, prefs)) return;

        // Step 3: Deduplicate and aggregate
        var dedupeKey = $"notif:{targetUserId}:{evt.Type}:{evt.TweetId}";
        var existingCount = await _redis.IncrAsync(dedupeKey);
        await _redis.ExpireAsync(dedupeKey, TimeSpan.FromMinutes(10));

        if (existingCount == 1)
        {
            // First notification of this type for this tweet
            await SendNotificationAsync(targetUserId, evt);
        }
        else if (existingCount == 10 || existingCount == 100
              || existingCount == 1000 || existingCount == 10000)
        {
            // Aggregated milestone notification
            await SendAggregatedNotificationAsync(targetUserId, evt.Type,
                evt.TweetId, existingCount);
        }
    }

    private async Task SendNotificationAsync(long userId, EngagementEvent evt)
    {
        // Store in notification center
        await _notificationStore.InsertAsync(new Notification
        {
            NotificationId = SnowflakeIdGenerator.NextId(),
            UserId = userId,
            Type = evt.Type,
            ActorUserId = evt.ActorUserId,
            TweetId = evt.TweetId,
            CreatedAt = DateTime.UtcNow,
            IsRead = false
        });

        // Send push notification
        var deviceTokens = await _pushService.GetDeviceTokensAsync(userId);
        foreach (var token in deviceTokens)
        {
            await _pushService.SendPushAsync(new PushMessage
            {
                DeviceToken = token,
                Title = GetNotificationTitle(evt),
                Body = GetNotificationBody(evt),
                Data = new Dictionary<string, string>
                {
                    ["type"] = evt.Type.ToString(),
                    ["tweetId"] = evt.TweetId.ToString(),
                    ["actorId"] = evt.ActorUserId.ToString()
                }
            });
        }
    }

    private async Task SendAggregatedNotificationAsync(
        long userId, NotificationType type, long tweetId, int count)
    {
        var message = type switch
        {
            NotificationType.Like =>
                $"@{await GetUsernameAsync(userId)} and {count:N0} others liked your tweet",
            NotificationType.Retweet =>
                $"Your tweet was retweeted {count:N0} times",
            NotificationType.Follow =>
                $"You have {count:N0} new followers",
            _ => $"Your tweet has {count:N0} new engagements"
        };

        await _pushService.SendBulkPushAsync(userId, new PushMessage
        {
            Title = "Twitter",
            Body = message,
            Priority = PushPriority.Low
        });
    }
}

Notification Delivery Channels

ChannelLatency TargetThroughputTechnology
Push (iOS)< 2 seconds50M pushes/dayApple APNs (HTTP/2 multiplexed)
Push (Android)< 2 seconds80M pushes/dayFirebase Cloud Messaging
In-app center< 1 second100K QPS readsRedis sorted set + Cassandra
Email digestBatched hourly/daily10M emails/daySES with Kafka batching
Web push< 5 seconds20M pushes/dayWeb Push API + VAPID

14. Media Storage and Delivery

Media content — images, GIFs, and videos — constitutes the majority of Twitter's storage and bandwidth. Users upload over 200 million images and 20 million videos daily. Each image must be stored in its original format and also processed into multiple thumbnail sizes for different client devices. Videos must be transcoded into multiple resolutions and bitrates for adaptive streaming. All media is served through a CDN to minimize latency and reduce origin server load.

The media upload pipeline works as follows: the client uploads the media file to the Media Service, which stores it in an object store (S3), generates thumbnails and transcodes videos asynchronously, and returns a media ID that the client includes in the tweet. The Media Service generates unique URLs for each media asset and configures CDN caching with appropriate TTLs. Images are served with WebP format for modern browsers and JPEG for older ones, reducing bandwidth by 30-50%.

// Media Upload Service - C# Implementation
public class MediaService
{
    private readonly IObjectStore _objectStore;
    private readonly IImageProcessor _imageProcessor;
    private readonly IVideoTranscoder _videoTranscoder;
    private readonly ICDNService _cdn;

    private static readonly string[] ThumbnailSizes = { "small", "medium", "large" };

    public async Task<MediaResult> UploadMediaAsync(
        Stream fileStream, string contentType, long userId)
    {
        var mediaId = SnowflakeIdGenerator.NextId();
        var ext = GetExtension(contentType);
        var basePath = $"media/{userId}/{mediaId}";

        // Step 1: Store original file
        var originalKey = $"{basePath}/original{ext}";
        await _objectStore.PutObjectAsync(originalKey, fileStream, contentType);

        // Step 2: Generate thumbnails for images
        if (IsImage(contentType))
        {
            foreach (var size in ThumbnailSizes)
            {
                var thumbnailStream = await _imageProcessor.ResizeAsync(
                    fileStream, GetDimensions(size));
                var thumbKey = $"{basePath}/{size}{ext}";
                await _objectStore.PutObjectAsync(thumbKey, thumbnailStream, contentType);
            }

            // Generate WebP version for modern browsers
            var webpStream = await _imageProcessor.ConvertToWebPAsync(fileStream);
            await _objectStore.PutObjectAsync($"{basePath}/original.webp", webpStream, "image/webp");
        }

        // Step 3: Transcode video asynchronously
        if (IsVideo(contentType))
        {
            await _videoTranscoder.EnqueueAsync(new TranscodeJob
            {
                MediaId = mediaId,
                SourceKey = originalKey,
                OutputPath = basePath,
                Resolutions = new[] { "360p", "480p", "720p", "1080p" }
            });
        }

        // Step 4: Generate CDN URLs
        var cdnUrl = _cdn.GetPublicUrl(originalKey);

        return new MediaResult
        {
            MediaId = mediaId,
            Url = cdnUrl,
            ThumbnailUrls = ThumbnailSizes.ToDictionary(
                s => s,
                s => _cdn.GetPublicUrl($"{basePath}/{s}{ext}")),
            ContentType = contentType,
            ProcessingStatus = IsVideo(contentType) ? "processing" : "ready"
        };
    }
}

Media Processing Pipeline

graph LR Upload["Client Upload"] --> Validate["Validate & Scan"] Validate --> Store["S3 Original"] Store --> ImageProcess["Image Processor"] Store --> VideoProcess["Video Transcoder"] ImageProcess --> Thumbnails["Generate Thumbnails"] ImageProcess --> WebP["Convert to WebP"] Thumbnails --> S3Thumb["S3 Thumbnails"] WebP --> S3WebP["S3 WebP"] VideoProcess --> HLS["Generate HLS Segments"] HLS --> S3Video["S3 Video Assets"] S3Thumb --> CDN["CloudFront CDN"] S3WebP --> CDN S3Video --> CDN CDN --> Client["Client"]

15. Caching Strategy — Multi-Layer Approach

Twitter's caching strategy is a multi-layer system designed to minimize database reads while maintaining acceptable staleness. The cache hierarchy consists of the client-side cache (local storage and HTTP cache headers), the CDN cache (for media and public profiles), the application cache (Redis clusters for timelines, tweets, and user data), and the database buffer pool. Each layer has different invalidation strategies and TTL values depending on the data's volatility and access patterns.

The most critical cache is the timeline cache in Redis. Twitter maintains per-user Redis sorted sets where the score is the tweet timestamp. The sorted set allows efficient range queries for pagination (ZREVRANGEBYSCORE) and automatic eviction of old tweets (ZREMRANGEBYRANK). Each user's timeline cache holds approximately 800 tweets (the most recent ones), which provides enough content for several pages of scrolling. The cache hit rate target is 95%+ for timeline reads, meaning only 5% of timeline requests fall through to the database.

// Multi-Layer Cache Service - C# Implementation
public class MultiLayerCacheService
{
    private readonly IMemoryCache _l1Cache;        // In-process, ~100MB
    private readonly IRedisCluster _l2Cache;       // Distributed, ~50TB
    private readonly ITweetRepository _database;   // Persistent storage

    private static readonly TimeSpan L1TTL = TimeSpan.FromSeconds(30);
    private static readonly TimeSpan L2TTL_Tweet = TimeSpan.FromHours(24);
    private static readonly TimeSpan L2TTL_User = TimeSpan.FromHours(1);
    private static readonly TimeSpan L2TTL_Timeline = TimeSpan.FromMinutes(5);

    public async Task<Tweet?> GetTweetAsync(long tweetId)
    {
        // L1: In-process memory cache (per server instance)
        var l1Key = $"tweet:{tweetId}";
        if (_l1Cache.TryGetValue<Tweet>(l1Key, out var l1Tweet))
            return l1Tweet;

        // L2: Redis distributed cache
        var l2Data = await _l2Cache.GetAsync(l1Key);
        if (l2Data != null)
        {
            var tweet = JsonSerializer.Deserialize<Tweet>(l2Data);
            _l1Cache.Set(l1Key, tweet, L1TTL);
            return tweet;
        }

        // L3: Database (cache miss)
        var dbTweet = await _database.GetTweetByIdAsync(tweetId);
        if (dbTweet != null)
        {
            await _l2Cache.SetAsync(l1Key,
                JsonSerializer.Serialize(dbTweet), L2TTL_Tweet);
            _l1Cache.Set(l1Key, dbTweet, L1TTL);
        }

        return dbTweet;
    }

    public async Task InvalidateTweetCacheAsync(long tweetId)
    {
        // Invalidate across all layers
        _l1Cache.Remove($"tweet:{tweetId}");
        await _l2Cache.DeleteAsync($"tweet:{tweetId}");

        // Also invalidate any timeline caches that might contain this tweet
        // (This is handled asynchronously via Kafka for performance)
    }
}

// Cache warming service - pre-populates cache during off-peak hours
public class CacheWarmingService
{
    public async Task WarmPopularTweetCacheAsync()
    {
        // Get top 100K most-viewed tweets from the last 24 hours
        var popularTweets = await _analyticsStore.GetTopTweetsAsync(
            period: TimeSpan.FromHours(24), limit: 100_000);

        // Pre-load into Redis
        var pipeline = _redis.CreatePipeline();
        foreach (var tweet in popularTweets)
        {
            pipeline.Set(
                $"tweet:{tweet.TweetId}",
                JsonSerializer.Serialize(tweet),
                TimeSpan.FromHours(24));
        }
        await pipeline.ExecuteAsync();
    }
}

Cache Layer Summary

LayerTechnologySizeTTLHit Rate Target
L1 (In-process)IMemoryCache (C#)100 MB per instance30 seconds40%
L2 (Distributed)Redis cluster50 TB across 1000 nodes5 min - 24 hours55%
L3 (CDN)CloudFrontUnlimited (media)24 hours90% for media
L4 (Database)Manhattan / MySQLPetabytesN/AFinal fallback

16. Database Sharding and Partitioning

At Twitter's scale, a single database instance cannot store all tweets, users, or follow relationships. The data must be sharded (horizontally partitioned) across thousands of machines. Twitter uses different sharding strategies for different data types, each optimized for the primary access pattern. The key challenge in sharding is choosing the right shard key: it must distribute data evenly across shards while co-locating data that is frequently queried together.

For the tweets table, Twitter shards by tweet_id using consistent hashing. Since tweet IDs are Snowflake-generated (roughly time-ordered), this ensures that recent tweets are distributed across all shards rather than concentrated on a single shard. For the follows table, Twitter shards by follower_id, co-locating all follow relationships for a given user on the same shard. This optimizes the "get who I follow" query needed for timeline generation. The users table is sharded by user_id with range-based partitioning for efficient range scans.

// Database Sharding Service - C# Implementation
public class ShardedTweetRepository : ITweetRepository
{
    private readonly List<IDatabaseConnection> _shardConnections;
    private const int ShardCount = 256;

    public ShardedTweetRepository(List<string> shardConnectionStrings)
    {
        _shardConnections = shardConnectionStrings
            .Select(connStr => new DatabaseConnection(connStr))
            .ToList();
    }

    private IDatabaseConnection GetShard(long tweetId)
    {
        // Consistent hashing on tweet_id
        var shardIndex = (int)(tweetId % ShardCount);
        return _shardConnections[shardIndex % _shardConnections.Count];
    }

    public async Task<Tweet?> GetTweetByIdAsync(long tweetId)
    {
        var shard = GetShard(tweetId);
        var command = new MySqlCommand(
            "SELECT * FROM tweets WHERE tweet_id = @id", shard.Connection);
        command.Parameters.AddWithValue("@id", tweetId);

        using var reader = await command.ExecuteReaderAsync();
        if (await reader.ReadAsync())
        {
            return MapToTweet(reader);
        }
        return null;
    }

    public async Task InsertAsync(Tweet tweet)
    {
        var shard = GetShard(tweet.TweetId);
        var command = new MySqlCommand(@"
            INSERT INTO tweets
                (tweet_id, user_id, content, media_urls, hashtags,
                 mentions, reply_to_tweet_id, retweet_of_tweet_id,
                 language, created_at)
            VALUES
                (@tweetId, @userId, @content, @mediaUrls, @hashtags,
                 @mentions, @replyTo, @retweetOf,
                 @language, @createdAt)", shard.Connection);

        command.Parameters.AddWithValue("@tweetId", tweet.TweetId);
        command.Parameters.AddWithValue("@userId", tweet.UserId);
        command.Parameters.AddWithValue("@content", tweet.Content);
        // ... additional parameters

        await command.ExecuteNonQueryAsync();
    }

    // Cross-shard query: Get all tweets by a user (for user timeline)
    public async Task<List<Tweet>> GetUserTweetsAsync(
        long userId, int limit = 20)
    {
        // Must query all shards in parallel (scatter-gather)
        var tasks = _shardConnections.Select(async shard =>
        {
            var command = new MySqlCommand(@"
                SELECT * FROM tweets
                WHERE user_id = @userId
                ORDER BY created_at DESC
                LIMIT @limit", shard.Connection);
            command.Parameters.AddWithValue("@userId", userId);
            command.Parameters.AddWithValue("@limit", limit);

            using var reader = await command.ExecuteReaderAsync();
            var tweets = new List<Tweet>();
            while (await reader.ReadAsync())
            {
                tweets.Add(MapToTweet(reader));
            }
            return tweets;
        });

        var results = await Task.WhenAll(tasks);
        return results
            .SelectMany(t => t)
            .OrderByDescending(t => t.CreatedAt)
            .Take(limit)
            .ToList();
    }
}

Sharding Strategy Comparison

TableShard KeyStrategyReason
tweetstweet_idConsistent hashingEven distribution; time-ordered IDs prevent hotspots
followsfollower_idHash-basedCo-locate all follow relationships for a user
usersuser_idRange-basedEfficient range scans for user ID lookups
likesuser_idHash-basedCheck "did I like this tweet?" is user-centric
timeline_cacheuser_idRedis hash slotsEach user's timeline is independent

17. Monitoring, Observability, and Reliability

Operating a system at Twitter's scale requires comprehensive monitoring and observability to detect issues before they impact users. Twitter uses a multi-layered observability stack: metrics (time-series data for dashboards and alerts), logs (structured logs for debugging and auditing), and distributed traces (end-to-end request tracing for latency analysis). The monitoring system must handle millions of data points per second and provide sub-second alerting for critical metrics like timeline latency, tweet posting success rate, and fanout lag.

The key metrics tracked across the Twitter infrastructure include: timeline read latency (p50, p95, p99), tweet posting latency, fanout lag (how far behind the fanout service is from real-time), cache hit rates across all layers, Kafka consumer lag for tweet events, search index freshness (time from tweet post to search availability), error rates per service, and database replication lag. Anomaly detection algorithms automatically alert on-call engineers when metrics deviate from baseline patterns.

// Monitoring and Observability Setup - C# Implementation
public class TwitterMetrics
{
    private readonly IMetricsCollector _metrics;

    public TwitterMetrics(IMetricsCollector metrics)
    {
        _metrics = metrics;
    }

    // Timeline read metrics
    public void RecordTimelineRead(long userId, double latencyMs, int tweetCount, bool fromCache)
    {
        _metrics.Histogram("timeline.read.latency_ms", latencyMs);
        _metrics.Histogram("timeline.read.tweet_count", tweetCount);
        _metrics.Counter("timeline.read.total", 1,
            ("cache_hit", fromCache.ToString()),
            ("user_type", GetUserType(userId)));
    }

    // Tweet posting metrics
    public void RecordTweetPost(double latencyMs, bool success, string errorType = null)
    {
        _metrics.Histogram("tweet.post.latency_ms", latencyMs);
        _metrics.Counter("tweet.post.total", 1,
            ("success", success.ToString()),
            ("error_type", errorType ?? "none"));
    }

    // Fanout metrics
    public void RecordFanoutOperation(int followerCount, double latencyMs, bool isCelebrity)
    {
        _metrics.Histogram("fanout.followers_affected", followerCount);
        _metrics.Histogram("fanout.latency_ms", latencyMs);
        _metrics.Counter("fanout.operations_total", 1,
            ("is_celebrity", isCelebrity.ToString()));
    }

    // Kafka consumer lag
    public void RecordConsumerLag(string topic, string consumerGroup, long lagMessages)
    {
        _metrics.Gauge("kafka.consumer_lag", lagMessages,
            ("topic", topic), ("consumer_group", consumerGroup));
    }

    // Cache metrics
    public void RecordCacheOperation(string layer, string operation, bool hit, double latencyMs)
    {
        _metrics.Counter("cache.operations_total", 1,
            ("layer", layer),
            ("operation", operation),
            ("hit", hit.ToString()));
        _metrics.Histogram("cache.latency_ms", latencyMs,
            ("layer", layer));
    }
}

// Alerting rules
public class AlertingRules
{
    public static readonly List<AlertRule> Rules = new()
    {
        new AlertRule
        {
            Name = "Timeline Latency High",
            Metric = "timeline.read.latency_ms",
            Condition = Condition.P99_GreaterThan(200),
            Severity = Severity.Critical,
            Notification = NotificationType.PagerDuty
        },
        new AlertRule
        {
            Name = "Fanout Lag Growing",
            Metric = "kafka.consumer_lag",
            Condition = Condition.GreaterThan(10000),
            Severity = Severity.Warning,
            Notification = NotificationType.Slack
        },
        new AlertRule
        {
            Name = "Cache Hit Rate Low",
            Metric = "cache.operations_total{hit=true}",
            Condition = Condition.Rate_LessThan(0.90),
            Severity = Severity.Warning,
            Notification = NotificationType.Slack
        },
        new AlertRule
        {
            Name = "Tweet Post Error Rate",
            Metric = "tweet.post.total{success=false}",
            Condition = Condition.Rate_GreaterThan(0.01),
            Severity = Severity.Critical,
            Notification = NotificationType.PagerDuty
        }
    };
}

SLO Targets

ServiceSLOTargetError Budget
Timeline ServiceAvailability99.99%4.38 min/month
Timeline ServiceLatency p99< 200ms1% of requests above
Tweet ServiceAvailability99.99%4.38 min/month
Tweet ServiceDurability99.999999%Zero data loss
Search ServiceIndex freshness< 3 seconds5% of queries stale
Notification ServiceDelivery99.9%43.8 min/month

18. Cost Estimation

Running a Twitter-like system at scale involves significant infrastructure costs. The major cost drivers are: compute (application servers and ML inference), storage (tweets, media, search indexes), caching (Redis clusters), database (sharded MySQL/Manhattan), message queues (Kafka), CDN (media delivery), and network bandwidth. A rough monthly cost estimate for a Twitter-scale system is $30-50 million per month, which aligns with Twitter's reported infrastructure spending of approximately $400 million per year before the 2022 acquisition.

ComponentUnitsUnit CostMonthly Cost
Application servers5,000 instances (c5.4xlarge)$0.68/hr$2.45M
Redis cache cluster2,000 nodes (r5.4xlarge)$0.504/hr$7.26M
Manhattan/MySQL databases3,000 nodes (r5.8xlarge)$1.008/hr$21.8M
Elasticsearch cluster500 nodes (r5.2xlarge)$0.252/hr$0.91M
Kafka cluster200 brokers (m5.2xlarge)$0.384/hr$0.55M
Object storage (S3)15 PB$0.023/GB/month$345K
CDN (CloudFront)160 TB/day transfer$0.085/GB$4.08M
Network bandwidth100 Gbps dedicated$0.02/GB$5.26M
ML inference (ranking)100 GPU instances (p3.2xlarge)$3.06/hr$2.2M
Monitoring & loggingCustom stack-$500K
Total Estimated Monthly~$45M
Cost Optimization Opportunities: Twitter can reduce costs by: (1) using spot instances for non-critical batch workloads like fanout processing, saving 60-70% on compute; (2) compressing timeline cache data with delta encoding, reducing Redis memory by 40%; (3) tiered storage for old tweets (SSD for recent, HDD for archival), reducing storage cost by 50%; (4) aggressive CDN caching for media with longer TTLs, reducing origin fetches by 80%.

19. Interview Q&A

Q1: Walk me through what happens when a user posts a tweet.

When a user posts a tweet, the client sends a POST request to the Tweet Service. The service validates the content (character limit, prohibited content), generates a Snowflake ID for the tweet, writes it to the Manhattan database, and publishes a TweetEvent to the Kafka tweet-events topic. The Fanout Service consumes the event and, based on the author's follower count, either pushes the tweet ID to all followers' Redis timeline caches (fanout-on-write for users with fewer than 5,000 followers) or stores it in the celebrity tweet cache (fanout-on-read for accounts with more than 5,000 followers). The Search Service also consumes the event and indexes the tweet in the Elasticsearch cluster. The entire write path returns success to the client in under 500ms because all downstream processing is asynchronous.

Q2: How would you design the fanout system to handle 100M+ follower accounts?

The key insight is to never use fanout-on-write for celebrity accounts. When an account like @elaborate Musk (100M+ followers) tweets, pushing to 100 million Redis sorted sets is prohibitively expensive and would take over 16 minutes even with aggressive parallelism. Instead, we use fanout-on-read: the tweet is stored once in the celebrity's Redis list. When a follower loads their timeline, the system fetches the top 10 tweets from each celebrity they follow and merges them with the pre-computed timeline. To optimize this, we maintain a set of celebrity users and track which users follow celebrities, so the merge only happens when needed. Request coalescing ensures that concurrent requests for the same celebrity timeline only trigger one backend fetch.

Q3: How do you ensure the timeline is fresh but the system remains available?

We use eventual consistency with a staleness budget. The fanout service targets a lag of less than 2 seconds between a tweet being posted and it appearing in followers' timelines. The Redis timeline caches are the source of truth for the home feed, and they are updated asynchronously via the fanout pipeline. If the fanout service falls behind (Kafka consumer lag grows), we serve stale timelines with a "recent" indicator. The system never blocks timeline reads to wait for fanout completion. We use circuit breakers to prevent cascading failures: if Redis is slow, we serve from L1 (in-memory) cache or return a simplified chronological feed without ranking.

Q4: How does Twitter search achieve sub-second indexing and retrieval?

Twitter search uses a two-tier index architecture. The top tier is an in-memory segment that indexes tweets within 1-3 seconds of posting. This segment holds the most recent 24 hours of tweets and handles time-range queries efficiently. The bottom tier is a persistent Elasticsearch cluster that holds the complete historical index. When a search query arrives, the system queries both tiers in parallel and merges the results. The index uses 48 shards distributed across hundreds of nodes, with custom routing that co-locates tweets from the same time period. The search ranking combines BM25 text relevance with engagement signals (likes, retweets, recency) to produce the final ranked results.

Q5: How would you handle a sudden traffic spike during a breaking news event?

Several mechanisms work together to handle viral traffic spikes. First, request coalescing prevents duplicate backend calls: if 10,000 requests for the same trending topic arrive simultaneously, only one Elasticsearch query is executed and the result is shared. Second, aggressive caching at the CDN and Redis layers means most requests are served from cache. Third, load shedding drops non-critical requests (like analytics and recommendation prefetches) to protect the core timeline and tweet posting paths. Fourth, auto-scaling provisions additional application server capacity within minutes. Fifth, the architecture is designed to degrade gracefully: during extreme load, we can disable timeline ranking (serve chronological) and reduce tweets per page from 20 to 10, cutting the read amplification in half.

Q6: Explain the difference between Manhattan and FlockDB in Twitter's stack.

Manhattan is Twitter's distributed key-value store used for general-purpose storage of tweets, user profiles, and engagement data. It provides multi-dimensional indexing, automatic sharding, and tunable consistency across data centers. Think of it as a highly scalable NoSQL database optimized for point lookups and range scans. FlockDB is a graph database specifically designed for storing follow relationships (social graph edges). It is optimized for shallow, wide traversals — "give me all followers of user X" or "give me all accounts that user Y follows." FlockDB stores edges in MySQL with a custom indexing layer that supports efficient adjacency list queries. The two systems serve different access patterns: Manhattan for general data storage, FlockDB for graph queries.

Q7: How does Twitter handle tweet deletion and its propagation through the system?

When a user deletes a tweet, the deletion must propagate through multiple systems: the Manhattan database (mark tweet as deleted), the timeline caches (remove from all followers' Redis sorted sets), the search index (remove from Elasticsearch), and the media storage (soft-delete associated images/videos). The deletion is published as a DeleteEvent to Kafka, and each downstream consumer handles its part of the cleanup. Timeline cache cleanup uses lazy deletion: the tweet is marked as deleted in the database, and the timeline service skips deleted tweets during rendering. Active timeline caches are cleaned up asynchronously via a background worker. Search index deletion uses Elasticsearch's delete-by-query API. The system ensures that deleted tweets do not appear in new timeline renders within 5 seconds, though they may remain in cached responses for up to 30 seconds.

Q8: How would you design the trending topics feature?

Trending topics are computed by analyzing the volume and velocity of hashtag usage in real-time. The system uses a sliding window algorithm: it counts the number of tweets containing each hashtag over the last 5 minutes and compares it to the baseline count from the previous day (same time window). Hashtags with a significant velocity increase (e.g., 10x above baseline) are flagged as trending. The implementation uses a two-pass approach: first, a Kafka Streams job computes per-hashtag counts using a 5-minute tumbling window. Second, a ranking service compares current counts to historical baselines and applies geographic and personalization filters. Trending topics are pre-computed and cached in Redis every minute, and the client fetches them from a dedicated API endpoint. For local trends, the system maintains per-country and per-city trend lists.

Q9: What are the key trade-offs in Twitter's architecture?

The primary trade-offs are: (1) Consistency vs. Availability — Twitter prioritizes availability; timelines can be 1-2 seconds stale but must always load. (2) Write amplification vs. read latency — fanout-on-write trades higher write costs (N writes per tweet) for O(1) timeline reads, while fanout-on-read trades expensive merge queries for constant write cost. The hybrid approach balances both. (3) Freshness vs. performance — the search index trades 1-3 seconds of indexing delay for batch-optimized write performance. (4) Storage cost vs. latency — keeping 800 tweets per user in Redis cache costs more memory but provides sub-millisecond timeline reads. (5) Ranking quality vs. latency — the ML ranking model improves relevance but adds 10-20ms of latency compared to simple chronological sorting.

Q10: How would you scale the system to support direct messages (DMs)?

Direct messages require a different architecture than the public tweet system because DMs are private, ordered, and require end-to-end encryption. The DM system uses a conversation-based data model: each DM belongs to a conversation (1-to-1 or group), and messages within a conversation are stored in chronological order. The storage layer uses Cassandra with a partition key of conversation_id, ensuring all messages in a conversation are co-located. For real-time delivery, DMs use WebSocket connections instead of polling. When a DM is sent, the server pushes it to all connected recipients via their WebSocket connections. If a recipient is offline, the DM is stored in Cassandra and delivered via push notification. The DM system must handle group conversations with up to 500 participants, which requires careful fanout to avoid amplification issues.

Q11: How does Twitter handle spam and abuse at scale?

Twitter uses a multi-layered approach to spam and abuse detection. At the content level, an ML classifier analyzes tweet text for spam patterns, offensive content, and misinformation. At the behavioral level, a velocity-based system detects accounts that tweet too fast, follow/unfollow rapidly, or send excessive DMs. At the graph level, the system identifies coordinated inauthentic behavior (bot networks) by analyzing follow patterns and engagement anomalies. At the infrastructure level, rate limiting at the API gateway prevents abuse of the tweet posting and search APIs. The spam classifier runs in real-time and can quarantine suspicious tweets within seconds. False positives are managed through an appeals process and periodic model retraining on labeled data.

Q12: Design the analytics pipeline for tweet engagement metrics.

The analytics pipeline processes billions of engagement events (views, likes, retweets, clicks) per day. Each engagement event is published to a Kafka analytics topic. A Spark Streaming job consumes the events and computes real-time aggregates: tweet view count, like count, retweet count, and engagement rate. These aggregates are stored in a time-series database (like InfluxDB or Druid) for real-time dashboards and in HDFS/S3 for historical analysis. The pipeline uses exactly-once semantics to prevent double-counting. For the tweet author, engagement metrics are updated in near-real-time (within 5 seconds) and displayed on the tweet. For analytics dashboards, metrics are aggregated at 1-minute, 5-minute, and 1-hour intervals. The pipeline also feeds the ML ranking model by providing training data on which tweets received high engagement.

Q13: How would you implement the "For You" algorithmic feed?

The "For You" feed uses a candidate generation and ranking architecture. Candidate generation pulls tweets from multiple sources: (1) tweets from followed accounts (pre-computed timeline), (2) tweets from accounts similar to those the user follows (collaborative filtering), (3) trending tweets in the user's geographic region, and (4) tweets that the user's friends engaged with (social proof). The candidate set (approximately 1,000 tweets) is then ranked by a deep learning model that considers user behavior history, tweet features, author features, and social graph signals. The model is trained on implicit feedback: tweets the user liked, retweeted, or spent time reading. The top 200 ranked tweets are cached, and the timeline service selects the final 20 based on a diversity function that ensures the feed is not dominated by a single topic or author.

Q14: Explain how Twitter handles multi-device synchronization.

Multi-device sync requires tracking which tweets each user has seen across all their devices. Twitter uses a "last seen ID" approach: each device reports its position in the timeline (the ID of the last tweet it displayed) to the server. The server stores the most recent last-seen ID across all devices. When the user opens the app on any device, the timeline shows unread tweets since the globally most recent last-seen ID. This means opening the app on a desktop marks tweets as "seen" on mobile too. The implementation uses a Redis hash per user storing last-seen IDs per device type. A merge function computes the global position as the maximum across all devices. For the "new tweets" indicator, the system counts tweets between the device's last-seen ID and the global last-seen ID.

Q15: How would you reduce the cost of running the system by 50%?

Cost reduction strategies include: (1) Compress timeline caches using delta encoding — store only the delta between consecutive tweet IDs instead of full IDs, reducing Redis memory by 40%. (2) Use tiered storage — move tweets older than 90 days from SSD-backed Manhattan to HDD-backed archival storage, reducing storage cost by 60%. (3) Optimize fanout threshold — increasing the celebrity threshold from 5,000 to 10,000 reduces fanout writes by 30% at the cost of slightly more on-demand reads. (4) Adopt spot instances for batch workloads — fanout processing, search indexing, and ML training can use spot instances at 70% discount. (5) Reduce CDN costs by serving WebP/AVIF images and compressing video at lower bitrates without perceptible quality loss. (6) Consolidate Redis instances by increasing cache density and using Redis Cluster mode to reduce overhead.

20. Conclusion

Designing a Twitter-like system requires solving several interconnected distributed systems challenges: the fanout problem for distributing tweets to millions of followers, the timeline generation problem for assembling a personalized feed, the search problem for full-text indexing across hundreds of billions of tweets, and the reliability problem for handling viral traffic spikes without cascading failures. The key architectural decisions — hybrid fanout, multi-layer caching, Kafka-based async processing, and polyglot persistence — are all driven by the fundamental tension between read performance and write amplification at massive scale.

The hybrid fanout strategy is the most important insight: by using fanout-on-write for regular accounts and fanout-on-read for celebrity accounts, the system achieves O(1) timeline reads for 99% of users while avoiding the prohibitive write costs of pushing tweets to millions of celebrity followers. The multi-layer caching strategy (L1 in-process, L2 Redis, L3 CDN, L4 database) ensures that 95%+ of timeline reads are served from cache with sub-millisecond latency. The Kafka-based async pipeline decouples the write path from the read path, allowing the system to absorb write spikes without impacting read performance.

For system design interviews, the Twitter question tests your ability to reason about trade-offs at every layer: consistency vs. availability, write amplification vs. read latency, ranking quality vs. response time, and storage cost vs. performance. A strong answer demonstrates not just the "what" but the "why" — explaining the reasoning behind each architectural decision and acknowledging the trade-offs. The C# code examples in this guide show production-grade implementations of the core services, from the tweet posting pipeline to the ranking algorithm to the multi-layer cache service.

Key Takeaways for Interviews:
  • Always clarify the scale: 500M tweets/day and 330M users drive different decisions than 1M tweets/day.
  • Explain the fanout trade-off clearly: push for regular users, pull for celebrities, hybrid is the answer.
  • Discuss cache hierarchy and invalidation — this is where most candidates fall short.
  • Show awareness of failure modes: thundering herds, cascading failures, and the need for circuit breakers.
  • Mention monitoring and SLOs — this distinguishes senior+ candidates from mid-level engineers.

Twitter's architecture has evolved significantly since its inception in 2006, moving from a monolithic Ruby on Rails application to a sophisticated microservices architecture powered by custom-built infrastructure (Manhattan, FlockDB, Earlybird). The lessons learned from operating this system at scale — particularly around fanout optimization, cache efficiency, and viral event handling — are directly applicable to any large-scale social platform, news feed, or real-time publishing system. Understanding these patterns will serve you well not only in system design interviews but also in building and operating production systems at scale.

Originally published on Ayodhyya. Last updated July 1, 2026.

© 2026 Ayodhyya. All rights reserved.