How to Design Twitter/X Microblogging System — A Senior+ Guide | Ayodhyya
Building timelines, trending topics, and real-time feeds at 400M+ user scale
1. Introduction — The Scale of Twitter/X
Twitter, now rebranded as X under Elon Musk's ownership, stands as one of the most influential social media platforms in human history. It has fundamentally changed how we consume news, communicate with public figures, and engage in global discourse. As a senior software engineer preparing for system design interviews or architecting a similar platform, understanding how to build a microblogging system at Twitter's scale is an essential skill that tests nearly every dimension of distributed systems knowledge.
Let us begin by understanding the sheer magnitude of what Twitter handles on a daily basis. The platform serves over 400 million monthly active users who collectively generate approximately 500 million tweets per day. During major events such as elections, World Cup matches, or breaking news situations, the system must handle enormous spikes in traffic. The 2024 Super Bowl, for example, saw peak throughputs exceeding 200,000 tweets per second. The platform must deliver these tweets to timelines in real-time, support search across hundreds of billions of tweets, compute trending topics, and power direct messages, all while maintaining sub-second response latencies for the vast majority of requests.
The fundamental data model appears deceptively simple. A user posts a short message of up to 280 characters (or more with X Premium), other users can follow that user, and they see the posted messages in a chronological or algorithmically ranked feed. Users can like tweets, retweet them, reply to them, and share media such as images, videos, and polls. Beneath this simplicity lies a deeply complex distributed system that must solve problems in data replication, cache invalidation, fan-out propagation, real-time streaming, content ranking, and global consistency.
In this comprehensive guide, we will walk through every major subsystem of a Twitter-like platform, from the data model and API design to fan-out strategies, trending topics computation, real-time streaming, database sharding, and multi-region deployment. We will use C# code examples, Mermaid diagrams, and detailed capacity calculations to ensure you can present a complete and confident design in any interview or production planning session.
We will explore how Twitter historically used fan-out on write to precompute timelines, how the platform evolved to use fan-out on read for celebrity accounts, and how modern algorithmic ranking systems have replaced purely chronological feeds. We will also examine how trending topics are computed in real-time using sliding window algorithms, how search is powered by inverted indices, and how direct messages are delivered with end-to-end encryption guarantees.
By the end of this article, you will have a thorough understanding of every component required to build a production-grade microblogging system capable of serving hundreds of millions of users with low latency, high availability, and strong consistency where it matters. This knowledge will serve you well not only in interviews but also in real-world system design decisions across any platform that involves social feeds, content distribution, or real-time data propagation.
The modern Twitter/X platform has evolved well beyond simple microblogging. It now encompasses long-form posts, audio spaces, video content, payments, creator monetization, and AI-powered content recommendations. However, the core system design fundamentals remain the same, and understanding the foundational architecture is essential before tackling these advanced features. This article focuses on the core microblogging capabilities while noting where extensions can be layered on top of the base architecture.
2. Functional and Non-Functional Requirements
Before diving into architecture, we must clearly define what the system needs to do and how it must perform. This is the foundation of any system design discussion and demonstrates structured thinking to interviewers.
Functional Requirements
The core functional requirements of a Twitter-like microblogging system can be organized into several categories based on user-facing and system-level capabilities.
| Feature | Description | Priority |
|---|---|---|
| Tweet Creation | Users can post tweets with up to 280 characters of text, hashtags, mentions, and media attachments | P0 — Critical |
| Home Timeline | Users see an ordered feed of tweets from accounts they follow, ranked chronologically or algorithmically | P0 — Critical |
| User Timeline | View all tweets posted by a specific user on their profile page | P0 — Critical |
| Follow / Unfollow | Users can follow or unfollow other users to control their feed content | P0 — Critical |
| Like | Users can like tweets to show appreciation; like counts are visible on tweets | P1 — High |
| Retweet / Repost | Users can retweet others' tweets to share them with their own followers | P1 — High |
| Reply | Users can reply to tweets, creating threaded conversations | P1 — High |
| Search | Full-text search across tweets, users, and hashtags with relevance ranking | P1 — High |
| Trending Topics | Real-time computation and display of trending hashtags and topics | P1 — High |
| Direct Messages | Private one-on-one and group messaging between users | P1 — High |
| Media Upload | Upload and serve images, videos, GIFs, and polls within tweets | P1 — High |
| Notifications | Push notifications for likes, retweets, replies, follows, and mentions | P1 — High |
| Lists | Curated groups of accounts whose tweets can be viewed as a separate timeline | P2 — Medium |
| Bookmarks | Users can save tweets to private bookmarks for later reading | P2 — Medium |
| Spaces (Audio) | Live audio conversations hosted by users, similar to Clubhouse | P3 — Low |
Non-Functional Requirements
The non-functional requirements define the quality attributes that the system must uphold. These are often the most critical part of a system design interview because they drive the architectural trade-offs you make throughout the design process.
| Requirement | Target | Notes |
|---|---|---|
| Availability | 99.99% (52 min downtime/year) | Twitter targets high availability with graceful degradation during failures |
| Latency (Timeline) | P99 < 200ms | Home timeline load must be fast for good user experience |
| Latency (Tweet Post) | P99 < 500ms | Tweet creation can have slightly higher latency |
| Latency (Search) | P99 < 500ms | Search results should return within half a second |
| Consistency | Eventual consistency | Strong consistency for follows and DMs, eventual for timelines |
| Durability | 99.999999% (8 nines) | Tweets must never be lost once posted |
| Throughput | 600K reads/sec, 12K writes/sec | Read-heavy workload with approximately 50:1 read-to-write ratio |
| Scalability | Horizontal scaling | System must scale linearly with user and traffic growth |
The non-functional requirements shape every decision we make going forward. Because availability is paramount, we will design for failure at every layer. Because latency budgets are tight, we will heavily leverage caching and precomputation. Because the system is read-heavy, we will optimize read paths even at the cost of write complexity. These constraints form the foundation upon which the entire architecture is built.
It is also important to note that the system must handle varying traffic patterns. Daily usage peaks during lunch hours and evening commutes, while major events can cause 10x to 100x normal traffic spikes. The architecture must be elastic enough to absorb these spikes without degradation. This means auto-scaling compute tiers, sharded data stores with hot partition detection, and circuit breakers that gracefully degrade non-essential features during extreme load.
3. Capacity Estimation
Capacity estimation is a critical skill for system design interviews and production planning. It helps us understand the hardware and infrastructure requirements, identify bottlenecks early, and make informed decisions about data storage, caching, and network bandwidth. Let us work through the math for a Twitter-like system serving 400 million monthly active users.
Write Path Estimation
If we have 400 million monthly active users (MAU) and assume approximately 200 million daily active users (DAU), we can estimate the write throughput. If each active user creates an average of 2 tweets per day, the total daily tweet creation volume is 400 million tweets per day. This translates to approximately 4,600 tweets per second on average. However, traffic is not evenly distributed throughout the day. During peak hours, we might see 3x to 5x the average rate, which means we must design the write path to handle approximately 15,000 to 23,000 tweets per second during peak times.
Read Path Estimation
The read path is significantly more demanding. If each of the 200 million daily active users checks their timeline an average of 10 times per day, that produces 2 billion timeline requests per day, or approximately 23,000 requests per second on average. During peak hours, this could reach 70,000 to 100,000 timeline requests per second. Each timeline request requires fetching 20 to 40 tweets with user metadata, which means the read path must serve roughly 1 to 3 million tweet reads per second during peak periods.
Storage Estimation
Each tweet consists of text content (up to 280 characters or approximately 560 bytes in UTF-8), metadata such as user ID, timestamp, like count, retweet count, and reply count (approximately 100 bytes), and optional media references (approximately 100 bytes for metadata, not the media itself). This gives us an average of 760 bytes per tweet. With 400 million tweets per day, the daily storage requirement is approximately 304 GB. Over a year, that is approximately 111 TB of tweet data alone. Including user profiles, social graph data, and engagement records, total annual storage reaches approximately 200 TB.
Bandwidth Estimation
On the read side, if each timeline request returns approximately 3 KB of tweet data (30 tweets averaging 100 bytes each plus metadata), 23,000 requests per second produces approximately 69 MB per second or 5.5 Gbps of read bandwidth. On the write side, 4,600 tweets per second at 760 bytes each produces approximately 3.5 MB per second or 28 Mbps of write bandwidth. This confirms the heavily read-biased nature of the system and the need for aggressive caching and content delivery networks.
| Metric | Value | Calculation |
|---|---|---|
| Monthly Active Users | 400 million | Given |
| Daily Active Users | 200 million | 50% of MAU |
| Tweets per Day | 400 million | 2 tweets per DAU |
| Avg Write TPS | ~4,600 | 400M / 86,400 seconds |
| Peak Write TPS | ~23,000 | 5x average |
| Avg Read TPS | ~23,000 | 2B timeline requests / 86,400 |
| Peak Read TPS | ~100,000 | 4.3x average |
| Daily Storage | ~304 GB | 400M tweets x 760 bytes |
| Annual Storage | ~111 TB | 304 GB x 365 |
| Read Bandwidth (Peak) | ~5.5 Gbps | 100K req/s x 3 KB |
| Write Bandwidth | ~28 Mbps | 4,600 req/s x 760 bytes |
4. Data Model Design
The data model is the backbone of the system. A well-designed data model ensures efficient queries, clear relationships between entities, and the ability to scale. For a microblogging system, the core entities are Users, Tweets, Follows, Likes, Retweets, Replies, Direct Messages, Lists, and Trends. Let us define each of these in detail.
User Entity
The User entity stores all profile information for a platform member. It must support lookups by user ID for profile pages and by username for authentication and URL routing. The user record includes the display name, a unique username, a hashed password, bio text, profile and header image URLs, follower and following counts, the account creation date, and verification status. These counts are denormalized for fast retrieval since they are displayed on every profile page and frequently updated.
Tweet Entity
The Tweet entity is the central data structure. Each tweet has a unique snowflake-generated ID that encodes the timestamp for efficient range queries. The tweet stores the author's user ID, the text content (up to 280 characters), an optional parent tweet ID for replies, an optional original tweet ID for retweets, a media array containing references to uploaded images or videos, engagement counts (likes, retweets, replies, views), hashtags extracted from the text, and mentions of other users. The tweet ID is generated using Twitter's snowflake algorithm, which produces 64-bit IDs that are roughly time-ordered and globally unique.
Follow Entity
The Follow entity represents a directed relationship between two users. It stores the follower ID (the user who initiates the follow), the followee ID (the user being followed), and the timestamp of when the follow occurred. This table is queried in two primary ways: finding all followees of a user (to build their timeline) and finding all followers of a user (to fan out tweets). Both queries must be efficient, so we maintain indexes on both the follower_id and followee_id columns.
CREATE TABLE users (
user_id BIGINT PRIMARY KEY,
username VARCHAR(30) UNIQUE NOT NULL,
display_name VARCHAR(50) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
bio VARCHAR(280),
profile_image VARCHAR(500),
header_image VARCHAR(500),
follower_count INT DEFAULT 0,
following_count INT DEFAULT 0,
tweet_count INT DEFAULT 0,
is_verified BOOLEAN DEFAULT FALSE,
is_private BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tweets (
tweet_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(user_id),
content VARCHAR(280),
parent_tweet_id BIGINT REFERENCES tweets(tweet_id),
original_tweet_id BIGINT REFERENCES tweets(tweet_id),
retweet_count INT DEFAULT 0,
like_count INT DEFAULT 0,
reply_count INT DEFAULT 0,
view_count BIGINT DEFAULT 0,
is_reply BOOLEAN DEFAULT FALSE,
is_retweet BOOLEAN DEFAULT FALSE,
language VARCHAR(10),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE follows (
follower_id BIGINT REFERENCES users(user_id),
followee_id BIGINT REFERENCES users(user_id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
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 CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, tweet_id)
);SQL
Lists and Trends Entities
Lists allow users to create curated groups of accounts. A list has an owner, a name, a description, and a set of member users. When a user views a list timeline, the system retrieves tweets from all members of that list and returns them in reverse chronological order. Trends store computed trending topics with volume, velocity, and geographic region data.
CREATE TABLE lists (
list_id BIGINT PRIMARY KEY,
owner_id BIGINT REFERENCES users(user_id),
name VARCHAR(255) NOT NULL,
description VARCHAR(500),
member_count INT DEFAULT 0,
is_private BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE list_members (
list_id BIGINT REFERENCES lists(list_id),
user_id BIGINT REFERENCES users(user_id),
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (list_id, user_id)
);
CREATE TABLE direct_messages (
message_id BIGINT PRIMARY KEY,
conversation_id BIGINT NOT NULL,
sender_id BIGINT REFERENCES users(user_id),
receiver_id BIGINT REFERENCES users(user_id),
content TEXT,
media_url VARCHAR(500),
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);SQL
5. API Design — REST and Streaming
A clean and well-documented API is essential for any platform. Twitter uses a combination of REST APIs for request-response interactions and Streaming APIs for real-time data delivery. The API design follows RESTful conventions with resource-oriented URLs, standard HTTP methods, JSON payloads, and consistent error responses.
Tweet Operations
POST /api/v1/tweets
GET /api/v1/tweets/{tweet_id}
DELETE /api/v1/tweets/{tweet_id}
GET /api/v1/timelines/home?cursor={cursor}&limit=30
GET /api/v1/users/{user_id}/tweets?cursor={cursor}&limit=30
POST /api/v1/tweets/{tweet_id}/like
DELETE /api/v1/tweets/{tweet_id}/like
POST /api/v1/tweets/{tweet_id}/retweet
DELETE /api/v1/tweets/{tweet_id}/retweet
POST /api/v1/tweets/{tweet_id}/reply
GET /api/v1/search/tweets?q={query}&lang={lang}&result_type=recent
GET /api/v1/trends/locations/{woeid}
POST /api/v1/users/{user_id}/follow
DELETE /api/v1/users/{user_id}/follow
GET /api/v1/users/{user_id}/followers?cursor={cursor}
GET /api/v1/users/{user_id}/following?cursor={cursor}HTTP
Streaming API
The Streaming API enables real-time delivery of tweets to connected clients. Twitter offers several streaming endpoints, including the User Stream, the Site Stream, and the Filtered Stream. The filtered stream is the most commonly used and supports adding and removing filter rules dynamically.
GET /api/v1/streaming/filtered
Headers: Authorization: Bearer {token}
Query: track=keyword1,keyword2
follow=user_id1,user_id2
POST /api/v1/streaming/rules
Body: { "add": [{"value": "javascript lang:en", "tag": "js-tweets"}] }
DELETE /api/v1/streaming/rules
Body: { "delete": { "ids": ["rule_id_1"] } }
GET /api/v1/streaming/sample
Headers: Authorization: Bearer {token}HTTP Streaming
Rate Limiting
| Endpoint Category | Rate Limit | Window | Scope |
|---|---|---|---|
| Tweet Creation | 200 requests | 15 minutes | Per user |
| Tweet Read | 900 requests | 15 minutes | Per user |
| Home Timeline | 15 requests | 15 minutes | Per user |
| User Timeline | 900 requests | 15 minutes | Per user |
| Search | 450 requests | 15 minutes | Per user |
| Follow | 40 requests | 24 hours | Per user |
| Like | 500 requests | 24 hours | Per user |
| Streaming | 1 connection | Continuous | Per user |
Rate limiting is implemented at the API Gateway layer using a distributed counter stored in Redis. Each request increments a counter for the user and endpoint combination. If the counter exceeds the limit within the window, the gateway returns a 429 Too Many Requests response with headers indicating the reset time.
6. High-Level Architecture
The high-level architecture of a Twitter-like system consists of several major layers: the client tier (mobile apps and web browsers), the edge/CDN tier for static content delivery, the API Gateway tier for authentication, rate limiting, and request routing, the application tier containing microservices for each major feature, the data tier with multiple database types optimized for different access patterns, and the messaging tier for asynchronous communication between services.
At the edge, a Content Delivery Network serves static assets such as JavaScript bundles, CSS files, profile images, and tweet media. The CDN cache is distributed globally across dozens of edge locations, ensuring that users in any region receive low-latency access to frequently requested content.
The Load Balancer distributes incoming traffic across multiple API Gateway instances. The API Gateway is the single entry point for all client requests. It handles authentication by validating OAuth tokens, enforces rate limits using distributed counters in Redis, and routes requests to the appropriate microservice based on the URL path and HTTP method.
The application tier consists of independent microservices, each responsible for a specific domain. The Tweet Service handles tweet CRUD operations. The Timeline Service generates and serves home timelines. The User Service manages user profiles. The Search Service powers full-text search using Elasticsearch. The Trend Service computes trending topics. The DM Service handles direct messaging. The Media Service manages file uploads. The Notification Service delivers push notifications. The Moderation Service applies safety policies.
The data tier uses polyglot persistence, choosing the best data store for each access pattern. MySQL stores the core relational data. Redis clusters cache timelines and social graph data. Elasticsearch provides full-text search. Apache Kafka handles event streaming. S3 stores media files. Cassandra stores direct messages with high write throughput.
7. Tweet Creation and Storage
Tweet creation is one of the most critical write paths in the system. When a user taps the tweet button, the request flows through several stages before the tweet appears on timelines.
Tweet Creation Flow
The fan-out process is handled asynchronously by the Timeline Service, which consumes the TweetCreated event from Kafka. This ensures that the tweet creation API returns quickly to the user while the more expensive fan-out operation happens in the background.
Snowflake ID Generation
Twitter's snowflake algorithm generates 64-bit IDs that encode the timestamp, datacenter ID, machine ID, and a sequence number. The ID structure ensures that IDs generated on the same machine are monotonically increasing, and IDs generated across different machines are approximately time-ordered.
public class SnowflakeIdGenerator
{
private readonly long _epoch = new DateTimeOffset(2024, 1, 1, 0, 0, 0,
TimeSpan.Zero).ToUnixTimeMilliseconds();
private readonly long _datacenterId;
private readonly long _machineId;
private long _sequence = 0L;
private long _lastTimestamp = -1L;
private readonly object _lock = new object();
private const int DatacenterIdBits = 5;
private const int MachineIdBits = 5;
private const int SequenceBits = 12;
private const long MaxDatacenterId = (1L << DatacenterIdBits) - 1;
private const long MaxMachineId = (1L << MachineIdBits) - 1;
private const long MaxSequence = (1L << SequenceBits) - 1;
private const int MachineIdShift = SequenceBits;
private const int DatacenterIdShift = SequenceBits + MachineIdBits;
private const int TimestampLeftShift =
SequenceBits + MachineIdBits + DatacenterIdBits;
public SnowflakeIdGenerator(long datacenterId, long machineId)
{
if (datacenterId > MaxDatacenterId || datacenterId < 0)
throw new ArgumentException($"Datacenter ID must be 0-{MaxDatacenterId}");
if (machineId > MaxMachineId || machineId < 0)
throw new ArgumentException($"Machine ID must be 0-{MaxMachineId}");
_datacenterId = datacenterId;
_machineId = machineId;
}
public long NextId()
{
lock (_lock)
{
var timestamp = GetCurrentTimestamp();
if (timestamp == _lastTimestamp)
{
_sequence = (_sequence + 1) & MaxSequence;
if (_sequence == 0)
timestamp = WaitNextMillis(_lastTimestamp);
}
else { _sequence = 0L; }
_lastTimestamp = timestamp;
return ((timestamp - _epoch) << TimestampLeftShift) |
(_datacenterId << DatacenterIdShift) |
(_machineId << MachineIdShift) |
_sequence;
}
}
private long GetCurrentTimestamp()
=> DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
private long WaitNextMillis(long lastTimestamp)
{
var timestamp = GetCurrentTimestamp();
while (timestamp <= lastTimestamp)
timestamp = GetCurrentTimestamp();
return timestamp;
}
public long ExtractTimestamp(long id)
=> (id >> TimestampLeftShift) + _epoch;
}C#
Tweet Storage Partitioning
Tweets are partitioned by tweet ID, which is approximately time-ordered due to the snowflake algorithm. This means recent tweets are distributed across a predictable set of partitions, while older tweets reside on partitions that may be cold and stored on cheaper storage tiers. Hot partitions (recent tweets) are replicated across multiple availability zones with synchronous replication for durability.
When the tweet is persisted, the system also extracts hashtags and mentions from the tweet content. Hashtags are stored in a separate table for efficient querying, and mentions trigger notification events published to Kafka. The denormalized counters on the author's user record are atomically incremented, and the tweet is added to the author's user timeline in Redis.
8. Fan-Out on Write vs Fan-Out on Read
The fan-out strategy is arguably the most critical architectural decision in a microblogging system. It determines how tweets propagate from the author to the timelines of their followers.
Fan-Out on Write (Push Model)
In fan-out on write, when a user publishes a tweet, the system immediately iterates over all of the author's followers and writes the tweet ID to each follower's precomputed timeline stored in Redis. When a follower loads their home timeline, the system simply reads from this precomputed list, which is extremely fast. The major advantage is read latency — timeline loads are O(1) lookups into a cache. The disadvantage is write amplification. If a user has 10 million followers, a single tweet write triggers 10 million Redis writes.
Fan-Out on Read (Pull Model)
In fan-out on read, tweets are not precomputed into follower timelines. Instead, when a user loads their home timeline, the system fetches the latest tweet IDs from all of the users they follow and merges them in reverse chronological order. The advantage is that writes are cheap — a tweet write is just a single database insert. The disadvantage is that timeline reads are expensive, especially for users who follow thousands of accounts.
Twitter's Hybrid Approach
Twitter uses a hybrid strategy that combines both approaches based on the author's follower count. For regular users with fewer than approximately 10,000 followers, the system uses fan-out on write. For celebrity accounts with more than 10,000 followers, the system uses fan-out on read. When a user loads their home timeline, the system reads the precomputed timeline from Redis and also fetches recent tweets from the celebrities they follow, merging them together.
| Aspect | Fan-Out on Write | Fan-Out on Read | Hybrid |
|---|---|---|---|
| Write Cost | High (N writes per tweet) | Low (1 write per tweet) | Moderate |
| Read Cost | Low (cache lookup) | High (merge N sources) | Low to Moderate |
| Write Latency | Higher (fan-out delay) | Lower | Moderate |
| Read Latency | Lower | Higher | Lower |
| Storage | High (redundant copies) | Low | Moderate |
| Best For | Regular users | Celebrities | Both |
9. Home Timeline Generation
The home timeline is the most read-intensive component of the system. Every time a user opens the app, the home timeline must be served within 200 milliseconds. The generation process combines precomputed data with on-demand fetches and applies algorithmic ranking before returning results to the client.
Timeline Assembly Process
When a user requests their home timeline, the Timeline Service performs the following steps: First, it retrieves the precomputed tweet IDs from the user's Redis timeline cache. Second, the service identifies celebrity accounts that the user follows and fetches their recent tweets directly from the tweet database. Third, the service merges the two sets of tweets, sorts them by the algorithmic ranking score (or by timestamp for chronological mode), and selects the top 30 tweets. Fourth, the service enriches each tweet with author profile data, media URLs, and engagement counts from the cache layer.
Timeline Storage in Redis
The precomputed timeline for each user is stored as a Redis sorted set, where the score is the tweet ID (which is time-ordered due to the snowflake algorithm) and the member is the tweet ID. This allows efficient range queries for pagination using the ZREVRANGEBYSCORE command. Each user's timeline is capped at approximately 800 tweet IDs. When new tweet IDs are added and the set exceeds this limit, the oldest IDs are trimmed.
The Redis timeline for a user who follows 500 accounts might contain 800 tweet IDs, consuming approximately 6.4 KB of memory. Across 200 million daily active users, the total Redis memory for timelines is approximately 1.3 TB, which fits comfortably within a multi-node Redis cluster using replica sharding.
Handling Cache Misses
If a user's timeline cache is empty (cold start for new users or after cache eviction), the Timeline Service falls back to a pure fan-out-on-read approach. It fetches the recent tweets from all accounts the user follows, merges them, and returns the results. The system also triggers a background job to rebuild the precomputed timeline cache so that subsequent requests can be served from the cache.
10. User Timeline and Tweet Retrieval
The user timeline displays all tweets posted by a specific user on their profile page. Unlike the home timeline, the user timeline does not require fan-out or precomputation because it only contains tweets from a single author.
User Timeline Query
When a visitor navigates to a user profile, the system queries the tweets table for all tweets authored by that user, ordered by tweet_id in descending order. Because tweet_id is time-ordered, this query naturally returns tweets in reverse chronological order. The query uses cursor-based pagination where the cursor is the last tweet_id seen by the client.
-- Fetch user timeline with cursor-based pagination
SELECT tweet_id, user_id, content, like_count, retweet_count,
reply_count, view_count, created_at
FROM tweets
WHERE user_id = @UserId
AND tweet_id < @Cursor
ORDER BY tweet_id DESC
LIMIT 30;
-- Fetch user profile
SELECT user_id, username, display_name, bio, profile_image,
header_image, follower_count, following_count, tweet_count,
is_verified, created_at
FROM users
WHERE user_id = @UserId;SQL
User Timeline Caching
User timelines are cached in Redis using a sorted set keyed by user_id. When a new tweet is created, the tweet_id is added to the author's user timeline cache. The cache stores the most recent 200 tweet IDs for each active user. For users with very high tweet volumes, the cache may be updated less frequently and rely on a TTL-based invalidation strategy.
The user profile data is cached separately in a Redis hash structure with a TTL of 5 minutes for frequently accessed profiles and 1 hour for rarely accessed profiles. When the user updates their profile, the cache is explicitly invalidated to ensure fresh data is served on the next request.
11. Trending Topics and Hashtags
Trending topics are one of Twitter's most recognizable features. The system must identify emerging topics in real-time, rank them by popularity and velocity, and present them to users personalized by location and interest.
Trend Computation Pipeline
Sliding Window Algorithm
The system maintains multiple sliding windows for each topic: a 5-minute window for immediate spikes, a 1-hour window for sustained trends, and a 24-hour window for long-running conversations. The trend score is computed as a weighted combination of the current window volume, the velocity (ratio of current to previous window), and the breadth (number of unique users contributing to the topic). Topics that show sudden spikes are promoted to the trending list even if their absolute volume is lower than established topics.
Geographic and Personalized Trends
Trending topics vary by geographic location. The system computes trends independently for different geographic regions using a hierarchy of geographic identifiers. A worldwide trend represents topics popular globally, while country-level and city-level trends capture local conversations. Personalization further refines the trending list by considering the user's follow graph, past engagement history, and declared interests.
public class TrendingTopicService
{
private readonly IRedisClient _redis;
private readonly ITrendRepository _trendRepo;
public async Task<List<TrendDto>> GetTrendsAsync(
string regionCode, int count = 30)
{
var cacheKey = $"trends:{regionCode}";
var cached = await _redis.ListRangeAsync(cacheKey, 0, count - 1);
if (cached.Length > 0)
return cached.Select(c => JsonSerializer
.Deserialize<TrendDto>(c)).ToList();
var trends = await _trendRepo.GetTopTrendsAsync(regionCode, count);
var serialized = trends.Select(t =>
JsonSerializer.Serialize(t)).ToArray();
await _redis.ListRightPushAsync(cacheKey, serialized);
await _redis.KeyExpireAsync(cacheKey, TimeSpan.FromMinutes(5));
return trends;
}
public async Task ComputeTrendsAsync()
{
var windowSize = TimeSpan.FromMinutes(5);
var currentWindow = await _trendRepo.GetTweetCountsAsync(windowSize);
var previousWindow = await _trendRepo
.GetTweetCountsAsync(windowSize, TimeSpan.FromMinutes(5));
var scores = new Dictionary<string, double>();
foreach (var (topic, count) in currentWindow)
{
var prevCount = previousWindow.GetValueOrDefault(topic, 1);
var velocity = (double)count / prevCount;
var breadth = await _trendRepo.GetUniqueUserCountAsync(topic, windowSize);
scores[topic] = count * velocity * Math.Log2(breadth + 1);
}
var topTrends = scores
.OrderByDescending(kvp => kvp.Value)
.Take(100)
.Select(kvp => new TrendingTopic
{
Topic = kvp.Key,
TweetVolume = currentWindow[kvp.Key],
VelocityScore = kvp.Value,
ComputedAt = DateTime.UtcNow
}).ToList();
await _trendRepo.SaveTrendsAsync(topTrends);
await _redis.KeyDeleteAsync("trends:*");
}
}C#
12. Search System — Inverted Index
Twitter's search system enables users to find tweets, users, and hashtags matching arbitrary queries. The search system must support full-text search across hundreds of billions of tweets with sub-second latency, powered by Elasticsearch inverted indices.
Inverted Index Architecture
Search Index Partitioning
The search index is partitioned across multiple Elasticsearch nodes using a time-based strategy. Recent tweets (last 7 days) are stored on hot nodes with SSD storage for fast query performance. Tweets from the last 30 days are on warm nodes with larger HDD storage. Tweets older than 30 days are moved to cold storage. This tiered storage approach balances query performance with storage cost.
| Search Feature | Query Syntax | Example |
|---|---|---|
| Keyword Search | word | system design |
| Exact Phrase | "phrase" | "distributed systems" |
| Exclude Term | -word | java -javascript |
| From User | from:username | from:elonmusk |
| Hashtag | #hashtag | #systemdesign |
| Mention | @username | @ayodhyya |
| Date Range | since:date until:date | since:2026-01-01 |
| Min Likes | min_faves:N | min_faves:1000 |
| Language | lang:code | lang:en |
14. Like, Retweet, Reply Mechanics
Engagement actions — likes, retweets, and replies — are fundamental to the social experience. Each action updates engagement counts on the tweet, notifies the tweet author, and potentially affects timeline generation and trending computation.
| Action | DB Write | Counter Update | Notification | Fan-Out |
|---|---|---|---|---|
| Like | Insert into likes table | Increment tweet.like_count | Yes, to tweet author | No |
| Unlike | Delete from likes table | Decrement tweet.like_count | No | No |
| Retweet | Insert new tweet + update original | Increment original.retweet_count | Yes, to original author | Yes, to retweeter followers |
| Reply | Insert new tweet with parent_tweet_id | Increment parent.reply_count | Yes, to parent author | Yes, to replier followers |
| Quote Tweet | Insert new tweet + update original | Increment original.retweet_count | Yes, to original author | Yes, to quoter followers |
Liking a tweet inserts a row into the likes table and atomically increments the like_count on the tweet record. The like action also triggers a notification to the tweet author and a Kafka event that updates the tweet's engagement score for algorithmic ranking. Because the likes table uses a composite primary key of (user_id, tweet_id), duplicate likes are prevented at the database level.
A retweet creates a new tweet record with the original tweet's content, the retweeter as the author, and the original_tweet_id field pointing to the source tweet. The retweet action also increments the retweet_count on the original tweet. In the fan-out-on-write path, the retweet is pushed to the retweeter's followers' timelines.
A reply creates a new tweet with the parent_tweet_id set to the tweet being replied to. This creates a tree structure of replies rooted at the original tweet. The reply action increments the reply_count on the parent tweet and triggers a notification to the parent tweet author.
15. Direct Messages
Direct Messages (DMs) provide private communication between users. Unlike tweets, DMs are not broadcast and are only visible to the participants in the conversation. DMs must support text, media, read receipts, typing indicators, and group conversations.
DM Data Model
DMs are stored in Cassandra rather than MySQL because of the write-heavy access pattern. Each conversation has a unique conversation_id, and messages within a conversation are sorted by timestamp. Cassandra's wide-column model maps naturally to this access pattern: the partition key is conversation_id, and messages are stored as clustering columns sorted by message timestamp.
DM Delivery
When a user sends a DM, the DM Service writes the message to Cassandra, publishes a MessageSent event to Kafka, and pushes the message to the recipient through their WebSocket connection if they are online. The Notification Service consumes the MessageSent event and sends a push notification if the recipient is offline. For group conversations, the DM Service fans out the message to all participants.
End-to-End Encryption
For enhanced privacy, Twitter/X supports optional end-to-end encryption for DMs. The encryption keys are exchanged using a protocol similar to Signal's X3DH key agreement. Messages are encrypted on the sender's device and decrypted on the recipient's device. The server only stores encrypted ciphertext and cannot read the message content.
DM conversations support several additional features beyond plain text. Users can send images, videos, GIFs, and voice notes through the same media upload pipeline used for tweets. Read receipts indicate when a message has been seen, and typing indicators show when the other participant is composing a reply. These real-time features are delivered through WebSocket connections maintained between active clients and the DM WebSocket gateway.
16. Media Upload Pipeline
Media uploads — images, videos, GIFs, and polls — require a multi-stage processing pipeline that handles upload, virus scanning, transcoding, thumbnail generation, and CDN distribution.
Upload Flow
The client initiates a media upload by requesting a pre-signed upload URL from the Media Service. The client then uploads the file directly to S3 using the pre-signed URL, bypassing the application servers for the actual data transfer. For large video files, the upload uses chunked transfer encoding so that partial uploads can be resumed if the connection drops.
Processing Pipeline
The processing pipeline runs on a fleet of worker nodes that consume upload events from a dedicated Kafka topic. Images are resized to multiple dimensions (thumbnail, medium, large) and compressed using WebP format. Videos are transcoded to HLS format with multiple bitrate variants for adaptive streaming. GIFs are optimized and converted to short MP4 videos for better playback performance.
Media Serving
Processed media files are served through the CDN, which caches files at edge locations worldwide. Images use aggressive caching (24-hour TTL) since they are immutable once processed. Video content uses shorter cache TTLs (1 hour) because new transcoded variants may be added over time. The CDN configuration includes CORS headers for cross-origin access and cache invalidation capabilities for content moderation removals.
17. Algorithmic Feed Ranking
Since 2016, Twitter has moved away from a purely chronological feed toward an algorithmically ranked timeline. The algorithm selects and orders tweets based on predicted user engagement, personalizing the feed for each individual user.
Ranking Architecture
The ranking pipeline operates in several stages. First, a candidate generation stage selects a broad set of potentially relevant tweets from the user's follow graph and trending topics, producing approximately 800 to 1,500 candidate tweets. Second, a feature extraction stage computes features for each candidate tweet. Third, a machine learning model scores each candidate based on the probability that the viewer will engage with it. Fourth, a mixing stage interleaves ranked tweets with advertisements, recommended tweets, and suggested accounts.
public class FeedRanker
{
private readonly IMLModel _engagementModel;
private readonly IRedisClient _redis;
public async Task<List<RankedTweet>> RankTweetsAsync(
long userId, List<TweetCandidate> candidates)
{
var features = await ExtractFeaturesAsync(userId, candidates);
var predictions = _engagementModel.Predict(features);
var ranked = candidates.Zip(predictions, (tweet, pred) =>
new RankedTweet
{
Tweet = tweet,
Score = ComputeScore(pred),
LikeProb = pred.LikeProbability,
RetweetProb = pred.RetweetProbability,
ReplyProb = pred.ReplyProbability,
DwellTime = pred.ExpectedDwellTime
})
.OrderByDescending(r => r.Score)
.ToList();
return ranked;
}
private double ComputeScore(EngagementPrediction pred)
{
return (pred.LikeProbability * 1.0) +
(pred.RetweetProbability * 2.0) +
(pred.ReplyProbability * 1.5) +
(pred.ExpectedDwellTime / 60.0 * 0.5);
}
private async Task<List<float[]>> ExtractFeaturesAsync(
long userId, List<TweetCandidate> tweets)
{
var userFeatures = await GetUserFeaturesAsync(userId);
var result = new List<float[]>();
foreach (var tweet in tweets)
{
var tweetFeatures = await GetTweetFeaturesAsync(tweet.TweetId);
var authorFeatures = await GetAuthorFeaturesAsync(tweet.AuthorId);
var interactionFeatures = await GetInteractionFeaturesAsync(
userId, tweet.AuthorId);
var combined = userFeatures
.Concat(tweetFeatures)
.Concat(authorFeatures)
.Concat(interactionFeatures)
.ToArray();
result.Add(combined);
}
return result;
}
}C#
Engagement Prediction Model
The ranking model predicts the probability of several engagement actions: like, retweet, reply, and dwell time. The final ranking score is a weighted combination of these predictions. The model is trained on historical engagement data using gradient-boosted decision trees (XGBoost) or deep neural networks, and retrained daily on fresh data.
Content Quality Signals
The ranking model incorporates content quality signals to demote low-quality content. Tweets from accounts with a history of policy violations receive lower scores. Tweets flagged by the moderation system are downranked or removed. Tweets with excessive hashtag use, all-caps text, or engagement bait patterns receive penalty scores. Conversely, tweets from verified accounts, original content, and tweets with thoughtful replies receive positive quality signals.
18. Content Moderation and Safety
Content moderation at scale is one of the most challenging problems in social media. The system must detect and act on policy-violating content including hate speech, harassment, misinformation, spam, and explicit material.
Automated Moderation Pipeline
The automated moderation pipeline applies multiple layers of analysis to every tweet before it is published. The first layer uses lightweight regex and keyword matching to detect obvious policy violations. The second layer applies machine learning classifiers trained on labeled datasets to detect more nuanced violations like subtle harassment and misinformation. The third layer uses image and video analysis models to detect explicit content, violence, and manipulated media (deepfakes).
Human Review Queue
Tweets that are flagged by the automated pipeline but not confident enough for automatic removal are sent to a human review queue. Moderators review queued content and make decisions that are fed back into the training data for the ML classifiers. User reports also enter this queue, prioritized by the reporter's credibility score and the severity of the reported violation.
Rate-Based Spam Detection
Spam detection uses behavioral signals in addition to content analysis. Accounts that post an abnormally high volume of tweets, follow and unfollow large numbers of accounts in rapid succession, or post identical content across multiple tweets are flagged for spam review. The system maintains per-account behavioral profiles and applies anomaly detection algorithms to identify accounts whose behavior deviates significantly from normal patterns.
The moderation system must balance responsiveness with fairness. False positives erode user trust, while false negatives expose the platform to legal and reputational risk. The system provides an appeals process for users whose content is removed, and the appeals decisions are used to continuously improve the accuracy of the automated moderation models.
19. Real-Time Streaming — WebSocket and SSE
Real-time delivery of tweets, notifications, and direct messages requires persistent connections between clients and the server. Twitter supports two primary streaming technologies: WebSockets for full-duplex communication and Server-Sent Events (SSE) for one-way server-to-client delivery.
WebSocket Architecture
Each connected client maintains a WebSocket connection with one of the WebSocket gateway servers. The gateway servers maintain the in-memory mapping of user_id to WebSocket connection. When a tweet is published by a followed account, the fan-out service publishes a UserTimelineEvent to a Kafka topic partitioned by user_id. The WebSocket gateway consumes these events and pushes the tweet to all connected clients for that user.
Connection Management
The WebSocket gateway servers handle connection lifecycle management, including authentication, heartbeat monitoring, and graceful reconnection. Each connection is authenticated during the WebSocket handshake using an OAuth token. Heartbeat messages are exchanged every 30 seconds to detect dead connections. If a heartbeat response is not received within 10 seconds, the connection is terminated and the user is marked as offline.
Backpressure and Flow Control
When a connected client cannot keep up with the incoming message rate, the gateway applies backpressure by buffering messages and dropping lower-priority updates. Timeline updates (new tweets) are higher priority than notification updates (like counts). If the buffer fills completely, the gateway disconnects the client, forcing a reconnection that resets the message state.
Server-Sent Events Fallback
For clients that cannot maintain WebSocket connections (e.g., corporate firewalls that block WebSocket upgrades), the system provides a Server-Sent Events (SSE) fallback. SSE uses standard HTTP, which passes through most firewalls and proxies. The trade-off is that SSE is unidirectional — the client cannot send messages back through the SSE connection.
public class WebSocketGateway
{
private readonly ConcurrentDictionary<long, WebSocketConnection>
_connections = new();
private readonly IKafkaConsumer _kafkaConsumer;
public async Task HandleConnectionAsync(
WebSocket socket, long userId)
{
var connection = new WebSocketConnection(socket, userId);
_connections.AddOrUpdate(userId, connection,
(_, _) => connection);
await SendWelcomeMessageAsync(connection);
var receiveTask = ReceiveMessagesAsync(connection);
var heartbeatTask = HeartbeatLoopAsync(connection);
await Task.WhenAny(receiveTask, heartbeatTask);
_connections.TryRemove(userId, out _);
await socket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Connection closed", CancellationToken.None);
}
private async Task HeartbeatLoopAsync(WebSocketConnection conn)
{
while (conn.Socket.State == WebSocketState.Open)
{
var ping = Encoding.UTF8.GetBytes(
JsonSerializer.Serialize(new { type = "ping" }));
await conn.Socket.SendAsync(
new ArraySegment<byte>(ping),
WebSocketMessageType.Text, true,
CancellationToken.None);
await Task.Delay(TimeSpan.FromSeconds(30));
}
}
}C#
21. Caching Strategy — Redis and CDN
Caching is critical for meeting the latency requirements of a Twitter-like system. Multiple layers of caching reduce the load on databases and improve response times for the most frequently accessed data.
Redis Cache Layers
Redis serves as the primary caching layer for dynamic data. Different Redis clusters are used for different data types to prevent contention and allow independent scaling. The timeline Redis cluster stores precomputed home timelines as sorted sets. The social graph Redis cluster stores follower and following lists as Redis sets. The user profile Redis cluster stores user profile data as Redis hashes. The trends Redis cluster stores trending topics as Redis sorted sets.
Cache Invalidation Strategy
Cache invalidation is one of the hardest problems in distributed systems. Twitter uses a combination of TTL-based expiration and event-driven invalidation. Timeline caches use a fixed-size window (800 tweets) and do not require explicit invalidation. User profile caches use a 5-minute TTL with explicit invalidation when the profile is updated. Social graph caches are invalidated on follow/unfollow events. Trending caches use a 5-minute TTL and are fully refreshed on each computation cycle.
CDN Strategy
The CDN caches all static content including JavaScript bundles, CSS files, profile images, tweet media, and favicon. Immutable assets (hashed JS bundles) use aggressive caching with a 1-year TTL. Profile images use a 24-hour TTL with cache invalidation on upload. Tweet media uses a 1-hour TTL for recent content and longer TTLs for older content. The CDN handles approximately 90% of static content requests.
| Cache Layer | Data Type | Storage | TTL | Eviction |
|---|---|---|---|---|
| Home Timeline | Tweet IDs (sorted set) | Redis Cluster | No TTL | Fixed size (800 per user) |
| User Timeline | Tweet IDs (sorted set) | Redis Cluster | No TTL | Fixed size (200 per user) |
| User Profile | Profile data (hash) | Redis Cluster | 5 minutes | Explicit invalidation |
| Social Graph | Follower/following IDs | Redis Cluster | No TTL | Event-driven invalidation |
| Trending Topics | Trend data (sorted set) | Redis Cluster | 5 minutes | Full refresh |
| Tweet Objects | Tweet data (hash) | Redis Cluster | 1 hour | LRU |
| Search Results | Query results (list) | Redis Cluster | 5 minutes | LRU |
| CDN Cache | Static assets, media | CloudFront CDN | 1 hour to 1 year | LRU at edge |
22. Multi-Region Deployment
Twitter serves users worldwide, requiring a multi-region deployment that provides low-latency access regardless of geographic location. The system must handle cross-region data replication, failover during regional outages, and routing of users to the nearest region.
Region Architecture
Twitter operates multiple data center regions across the globe. Each region contains a full stack of services and data stores, capable of serving all user requests independently. A primary region handles all write operations for a given set of users, while replica regions handle read operations. The primary region replicates data to replica regions using asynchronous replication with a target lag of less than 100 milliseconds.
Write Routing
All write operations (tweet creation, follow, like) are routed to the primary region for the affected user. The primary region is determined by the user's home region assignment, which is stored in the user profile. If the primary region is unavailable, a failover mechanism promotes a replica region to primary and reroutes writes using consensus protocols (Raft or Paxos).
Read Routing
Read operations are routed to the nearest region using DNS-based geographic routing or Anycast IP. This ensures that timeline loads, profile views, and search queries are served from the closest data center, minimizing network latency. Because read replicas may lag behind the primary by up to 100 milliseconds, users may occasionally see slightly stale data.
23. Cost Estimation
Operating a Twitter-scale system requires significant infrastructure investment. This section provides rough cost estimates for the major infrastructure components.
Compute Costs
The application tier requires thousands of server instances to handle the request throughput. At Twitter's scale, the application tier might use approximately 5,000 application server instances (16 vCPU, 64 GB RAM each) for the API and timeline services, 2,000 instances for the search and ranking services, and 1,000 instances for background processing. At cloud pricing of approximately $0.10 per vCPU-hour, the monthly compute cost is approximately $2.5 to $3.5 million.
Storage Costs
The MySQL database cluster requires approximately 200 TB of SSD storage ($20,000/month). Redis clusters require approximately 10 TB of memory ($50,000/month). Elasticsearch clusters require approximately 500 TB of SSD storage ($50,000/month). S3 storage for media files requires approximately 500 TB ($12,000/month). Kafka clusters with approximately 500 brokers cost approximately $100,000/month.
Bandwidth Costs
The CDN and network bandwidth costs are driven by content volume. At 100,000 requests per second with 3 KB average response size, read bandwidth is approximately 5.5 Gbps. Including media content delivery through the CDN, total outbound bandwidth is approximately 20 Gbps. Monthly CDN cost is approximately $100,000.
| Component | Quantity | Monthly Cost |
|---|---|---|
| Application Servers (Compute) | ~8,000 instances | $2,500,000 - $3,500,000 |
| MySQL Cluster (SSD) | ~200 TB | $20,000 |
| Redis Cluster (Memory) | ~10 TB RAM | $50,000 |
| Elasticsearch Cluster | ~500 TB SSD | $50,000 |
| S3 / Blob Storage (Media) | ~500 TB | $12,000 |
| Kafka Cluster | ~500 brokers | $100,000 |
| CDN Bandwidth | ~20 Gbps | $100,000 |
| Internal Network | Cross-AZ traffic | $30,000 |
| Monitoring and Logging | Full observability | $80,000 |
| Support and Licensing | Enterprise support | $50,000 |
| Total Estimated | $3 - $4 Million/month |
24. Interview Q&A
System design interviews test not only your technical knowledge but also your communication skills and ability to reason about trade-offs. The following questions cover the most commonly asked topics related to Twitter/microblogging system design.
Q1: How would you design the home timeline for a user who follows 5,000 accounts?
Answer: For a user following 5,000 accounts, a pure fan-out-on-read approach would be too slow because it requires fetching and merging tweets from 5,000 separate user timelines. Instead, I would use the hybrid approach. The home timeline is precomputed using fan-out-on-write for all non-celebrity accounts the user follows. The precomputed timeline is stored in Redis as a sorted set of tweet IDs. When the user loads their timeline, I fetch the precomputed IDs from Redis (O(1) operation), then fetch recent tweets from any celebrity accounts they follow (typically fewer than 50 celebrities), merge the two sets, apply ranking, and return the top 30 tweets. The total latency is under 200 milliseconds.
Q2: How do you handle the celebrity tweet problem without degrading read performance?
Answer: The celebrity problem occurs when an account with millions of followers posts a tweet. The solution is to identify celebrity accounts (more than 10,000 followers) and use fan-out-on-read for their tweets. When a celebrity tweets, the tweet is stored in the database but not pushed to any follower's timeline. When any user loads their home timeline, the system fetches recent tweets from the celebrities they follow in parallel with the precomputed timeline lookup. Because there are relatively few celebrity accounts and each user follows a small fraction, the cost is manageable. Celebrity tweets can also be cached in a dedicated Redis sorted set per celebrity.
Q3: How would you design the trending topics system to handle 500 million tweets per day?
Answer: The trending topics system uses a stream processing pipeline built on Apache Flink. The Kafka tweet stream is consumed by Flink operators that extract hashtags and keywords. The operators maintain sliding window counts using Flink's built-in windowing primitives — a 5-minute window for immediate spikes, a 1-hour window for sustained trends, and a 24-hour window for ongoing topics. The trend score is computed as volume multiplied by velocity multiplied by breadth. The output is written to a Redis sorted set per geographic region. The computation runs continuously with a latency of less than 30 seconds from tweet creation to trend display.
Q4: How do you ensure tweet durability and never lose a posted tweet?
Answer: Tweet durability is ensured through multiple mechanisms. First, the tweet is written to the MySQL primary with synchronous replication to at least one replica before returning success to the client. Second, the tweet write is also published to Kafka, which provides its own replication and persistence guarantees. Third, the tweet is backed up to S3 periodically for long-term archival. Fourth, the MySQL database uses write-ahead logging (WAL) with periodic snapshots. The combination of synchronous replication, Kafka persistence, and S3 archival provides eight nines of durability.
Q5: How would you implement search across hundreds of billions of tweets?
Answer: Search is powered by Elasticsearch, which builds inverted indices over the tweet corpus. The index is partitioned by time (hot, warm, cold tiers) and sharded across multiple Elasticsearch nodes. Each tweet is indexed with its text content, hashtags, mentions, author ID, and timestamp. The indexing pipeline consumes tweet creation events from Kafka, tokenizes the text, applies language detection and stemming, and writes the document to the appropriate index shard. For search queries, the query is parsed into Elasticsearch DSL, executed across all relevant shards, and results are merged and ranked using BM25 scoring.
Q6: How would you handle a sudden traffic spike during a major event like the World Cup final?
Answer: Traffic during major events can spike 10x to 100x normal levels. The system handles this through several mechanisms: auto-scaling groups detect increased metrics and provision additional instances within minutes; the caching layer absorbs much of the read traffic because trending topics and popular tweets are naturally hot; circuit breakers gracefully degrade non-essential features; the CDN absorbs static content and media requests; and write traffic is buffered through Kafka. The combination of elastic compute, aggressive caching, graceful degradation, and message buffering keeps the system available during extreme spikes.
Q7: How would you design the follow/unfollow feature at scale?
Answer: The follow feature requires updating the social graph, incrementing counters, updating caches, and triggering fan-out jobs. The follows table uses a composite primary key (follower_id, followee_id) to prevent duplicates. Counter updates use atomic increment operations. The social graph Redis cache is updated asynchronously. On follow, the system triggers a fan-out job that adds the followee's recent tweets to the follower's home timeline cache. On unfollow, the system removes the relationship and lets tweets naturally expire from the cache. For celebrity accounts, no fan-out is triggered.
Q8: How do you prevent spam and bots from degrading the user experience?
Answer: Spam prevention uses a multi-layered approach. At the registration layer, new accounts must pass CAPTCHA and phone verification. At the behavior layer, rate limiting restricts the number of tweets, follows, and likes per time window. At the content layer, ML classifiers detect spam patterns. At the network layer, graph analysis identifies coordinated inauthentic behavior — clusters of accounts that follow the same targets and post similar content. At the reputation layer, new accounts with low follower counts have their content weighted less heavily in trending and search rankings.
Q9: How would you design direct messages with end-to-end encryption?
Answer: DMs use a Signal-inspired encryption protocol. When two users start a conversation, their devices perform a key exchange using the X3DH protocol. Each user has a long-term identity key, a medium-term signed pre-key, and one-time pre-keys registered with the server. The sender encrypts messages using AES-256-GCM with a shared secret derived from the key exchange. The server stores only the encrypted ciphertext and metadata. When the recipient comes online, the server pushes the ciphertext, and the recipient's device decrypts it using the shared secret. Key rotation happens periodically to maintain forward secrecy.
Q10: How do you design the system to handle deleted tweets gracefully?
Answer: Tweet deletion requires updates across multiple systems. The tweet is soft-deleted in the database (marked with a deleted_at timestamp) to maintain referential integrity for retweets and replies. The tweet is removed from all timeline caches by publishing a TweetDeleted event to Kafka. The tweet is removed from the search index. Retweets of the deleted tweet are also hidden. If the tweet had media, the files are moved to a deletion queue and permanently removed after a 30-day grace period.
Q11: How would you measure and monitor the health of the system?
Answer: The monitoring stack uses Prometheus for metrics collection, Grafana for dashboards, and PagerDuty for alerting. Key metrics include request latency (P50, P95, P99), error rates per service, cache hit rates, database connection pool utilization, Kafka consumer lag, and fan-out completion rates. SLIs are defined for each critical path: timeline load latency under 200ms at P99, tweet creation succeeding 99.99% of the time, and search latency under 500ms at P99. Error budgets allow teams to deploy confidently knowing that small increases in error rate are within acceptable bounds.
Q12: How would you migrate from a monolithic architecture to microservices without downtime?
Answer: The migration follows the strangler fig pattern. Each new microservice is built alongside the monolith, with an API gateway routing traffic to both. During the transition, both handle requests in parallel, and responses are compared to validate correctness. Once proven stable, traffic is gradually shifted using weighted routing. Data migration uses the dual-write pattern: both databases are written to simultaneously, with a reconciliation job ensuring consistency. The monolith code is deleted after all traffic is migrated, ensuring zero downtime.
25. Full C# Implementation
The following C# implementation provides a working reference for the core components of a Twitter-like microblogging system. It includes the data models, service interfaces, and key service implementations for tweet creation, timeline generation, fan-out, and trending topics.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microblogging.Core.Models
{
public class User
{
public long UserId { get; set; }
public string Username { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string Bio { get; set; } = string.Empty;
public string ProfileImageUrl { get; set; } = string.Empty;
public int FollowerCount { get; set; }
public int FollowingCount { get; set; }
public int TweetCount { get; set; }
public bool IsVerified { get; set; }
public DateTime CreatedAt { get; set; }
}
public class Tweet
{
public long TweetId { get; set; }
public long UserId { get; set; }
public string Content { get; set; } = string.Empty;
public long? ParentTweetId { get; set; }
public long? OriginalTweetId { get; set; }
public int LikeCount { get; set; }
public int RetweetCount { get; set; }
public int ReplyCount { get; set; }
public long ViewCount { get; set; }
public bool IsReply => ParentTweetId.HasValue;
public bool IsRetweet => OriginalTweetId.HasValue;
public List<string> MediaUrls { get; set; } = new();
public List<string> Hashtags { get; set; } = new();
public List<string> Mentions { get; set; } = new();
public DateTime CreatedAt { get; set; }
}
public class TrendingTopic
{
public string Topic { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public int TweetVolume { get; set; }
public double VelocityScore { get; set; }
public string RegionCode { get; set; } = "worldwide";
public DateTime ComputedAt { get; set; }
}
public class Notification
{
public long NotificationId { get; set; }
public long TargetUserId { get; set; }
public long SourceUserId { get; set; }
public string Type { get; set; } = string.Empty;
public long? TweetId { get; set; }
public bool IsRead { get; set; }
public DateTime CreatedAt { get; set; }
}
public class TweetCreatedEvent
{
public long TweetId { get; set; }
public long AuthorId { get; set; }
public string Content { get; set; } = string.Empty;
public bool IsReply { get; set; }
public bool IsRetweet { get; set; }
public long? ParentTweetId { get; set; }
public long? OriginalTweetId { get; set; }
public DateTime CreatedAt { get; set; }
}
}
namespace Microblogging.Core.Interfaces
{
using Models;
public interface ITweetRepository
{
Task<Tweet> InsertTweetAsync(Tweet tweet);
Task<Tweet?> GetTweetAsync(long tweetId);
Task<List<Tweet>> GetUserTimelineAsync(
long userId, long? cursor, int limit = 30);
Task<bool> DeleteTweetAsync(long tweetId);
Task<int> IncrementLikeCountAsync(long tweetId);
Task<int> DecrementLikeCountAsync(long tweetId);
Task<int> IncrementRetweetCountAsync(long tweetId);
Task<int> IncrementReplyCountAsync(long tweetId);
}
public interface IUserRepository
{
Task<User?> GetUserAsync(long userId);
Task<User?> GetUserByUsernameAsync(string username);
Task<User> CreateUserAsync(User user);
Task UpdateTweetCountAsync(long userId, int delta);
Task UpdateFollowerCountAsync(long userId, int delta);
Task UpdateFollowingCountAsync(long userId, int delta);
}
public interface IFollowRepository
{
Task<bool> FollowAsync(long followerId, long followeeId);
Task<bool> UnfollowAsync(long followerId, long followeeId);
Task<bool> IsFollowingAsync(long followerId, long followeeId);
Task<List<long>> GetFollowersAsync(
long userId, int limit = 1000);
Task<List<long>> GetFollowingAsync(
long userId, int limit = 1000);
}
public interface ITimelineCache
{
Task<List<long>> GetTimelineAsync(
long userId, int limit = 30);
Task PushToTimelineAsync(long userId, long tweetId);
Task RemoveFromTimelineAsync(long userId, long tweetId);
}
public interface IKafkaProducer
{
Task PublishAsync<T>(string topic, T message);
}
public interface ICacheClient
{
Task<T?> GetAsync<T>(string key);
Task SetAsync<T>(
string key, T value, TimeSpan? ttl = null);
Task<List<T>> GetSortedSetRangeAsync<T>(
string key, int start = 0, int stop = -1);
Task<long> AddToSortedSetAsync<T>(
string key, T value, double score);
Task<bool> SetAddAsync<T>(string key, T value);
Task<bool> SetContainsAsync<T>(string key, T value);
Task<long> IncrementAsync(string key, long delta = 1);
Task<List<string>> ListRangeAsync(
string key, int start = 0, int stop = -1);
Task<HashSet<long>> GetSetMembersAsync(string key);
}
public interface IContentModerator
{
Task<ModerationResult> ModerateAsync(
string content, long authorId);
}
public class ModerationResult
{
public bool IsApproved { get; set; }
public string Reason { get; set; } = string.Empty;
public double Confidence { get; set; }
}
public interface INotificationService
{
Task SendNotificationAsync(Notification notification);
}
public interface ISnowflakeIdGenerator
{
long NextId();
}
}C#
The interfaces defined above establish the contracts between services and their dependencies. Each interface is focused on a single responsibility and follows the Interface Segregation Principle. The repository interfaces abstract the data access layer, allowing implementations to use different databases without affecting the service logic. The cache client interface abstracts the caching layer, enabling easy switching between Redis implementations or local in-memory caches for testing.
The service layer implements the business logic using the interfaces defined above. The following code shows the Snowflake ID generator, the Tweet Service, the Follow Service, the Timeline Service, and the Trending Topic Service.
namespace Microblogging.Core.Services
{
using Interfaces;
using Models;
public class SnowflakeIdGenerator : ISnowflakeIdGenerator
{
private static readonly DateTime Epoch =
new(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private readonly long _datacenterId;
private readonly long _machineId;
private long _sequence = 0L;
private long _lastTimestamp = -1L;
private readonly object _lock = new();
private const int DatacenterIdBits = 5;
private const int MachineIdBits = 5;
private const int SequenceBits = 12;
private const long MaxDatacenterId =
(1L << DatacenterIdBits) - 1;
private const long MaxMachineId =
(1L << MachineIdBits) - 1;
private const long MaxSequence =
(1L << SequenceBits) - 1;
private const int MachineIdShift = SequenceBits;
private const int DatacenterIdShift =
SequenceBits + MachineIdBits;
private const int TimestampLeftShift =
SequenceBits + MachineIdBits + DatacenterIdBits;
public SnowflakeIdGenerator(
long datacenterId, long machineId)
{
if (datacenterId < 0 || datacenterId > MaxDatacenterId)
throw new ArgumentException("Invalid datacenter ID");
if (machineId < 0 || machineId > MaxMachineId)
throw new ArgumentException("Invalid machine ID");
_datacenterId = datacenterId;
_machineId = machineId;
}
public long NextId()
{
lock (_lock)
{
var timestamp = GetCurrentTimestamp();
if (timestamp == _lastTimestamp)
{
_sequence = (_sequence + 1) & MaxSequence;
if (_sequence == 0)
timestamp = WaitNextMillis(_lastTimestamp);
}
else { _sequence = 0L; }
_lastTimestamp = timestamp;
return ((timestamp - ToUnixMs(Epoch))
<< TimestampLeftShift) |
(_datacenterId << DatacenterIdShift) |
(_machineId << MachineIdShift) |
_sequence;
}
}
private static long GetCurrentTimestamp()
=> DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
private static long ToUnixMs(DateTime dt)
=> new DateTimeOffset(dt).ToUnixTimeMilliseconds();
private long WaitNextMillis(long lastTimestamp)
{
var ts = GetCurrentTimestamp();
while (ts <= lastTimestamp)
ts = GetCurrentTimestamp();
return ts;
}
}
public class TweetService
{
private readonly ITweetRepository _tweetRepo;
private readonly IUserRepository _userRepo;
private readonly ITimelineCache _timelineCache;
private readonly IKafkaProducer _kafka;
private readonly IContentModerator _moderator;
private readonly ISnowflakeIdGenerator _idGen;
private readonly INotificationService _notifService;
public TweetService(
ITweetRepository tweetRepo,
IUserRepository userRepo,
ITimelineCache timelineCache,
IKafkaProducer kafka,
IContentModerator moderator,
ISnowflakeIdGenerator idGen,
INotificationService notifService)
{
_tweetRepo = tweetRepo;
_userRepo = userRepo;
_timelineCache = timelineCache;
_kafka = kafka;
_moderator = moderator;
_idGen = idGen;
_notifService = notifService;
}
public async Task<Tweet> CreateTweetAsync(
long userId, string content,
long? parentTweetId = null,
List<string>? mediaUrls = null)
{
if (string.IsNullOrWhiteSpace(content))
throw new ArgumentException(
"Tweet content cannot be empty");
if (content.Length > 280)
throw new ArgumentException(
"Tweet exceeds 280 character limit");
var moderation = await _moderator
.ModerateAsync(content, userId);
if (!moderation.IsApproved)
throw new InvalidOperationException(
$"Tweet rejected: {moderation.Reason}");
var tweet = new Tweet
{
TweetId = _idGen.NextId(),
UserId = userId,
Content = content,
ParentTweetId = parentTweetId,
MediaUrls = mediaUrls ?? new List<string>(),
Hashtags = ExtractHashtags(content),
Mentions = ExtractMentions(content),
CreatedAt = DateTime.UtcNow
};
await _tweetRepo.InsertTweetAsync(tweet);
await _userRepo.UpdateTweetCountAsync(userId, 1);
await _timelineCache.PushToTimelineAsync(
userId, tweet.TweetId);
await _kafka.PublishAsync("tweet-created",
new TweetCreatedEvent
{
TweetId = tweet.TweetId,
AuthorId = userId,
Content = content,
IsReply = tweet.IsReply,
ParentTweetId = parentTweetId,
CreatedAt = tweet.CreatedAt
});
if (tweet.Mentions.Any())
{
foreach (var mention in tweet.Mentions)
{
await _notifService.SendNotificationAsync(
new Notification
{
NotificationId = _idGen.NextId(),
Type = "mention",
SourceUserId = userId,
TweetId = tweet.TweetId,
CreatedAt = DateTime.UtcNow
});
}
}
if (parentTweetId.HasValue)
{
var parentTweet = await _tweetRepo
.GetTweetAsync(parentTweetId.Value);
if (parentTweet != null)
{
await _tweetRepo.IncrementReplyCountAsync(
parentTweetId.Value);
await _notifService.SendNotificationAsync(
new Notification
{
NotificationId = _idGen.NextId(),
Type = "reply",
SourceUserId = userId,
TargetUserId = parentTweet.UserId,
TweetId = tweet.TweetId,
CreatedAt = DateTime.UtcNow
});
}
}
return tweet;
}
public async Task<Tweet> RetweetAsync(
long userId, long originalTweetId)
{
var original = await _tweetRepo
.GetTweetAsync(originalTweetId);
if (original == null)
throw new ArgumentException(
"Original tweet not found");
var retweet = new Tweet
{
TweetId = _idGen.NextId(),
UserId = userId,
Content = original.Content,
OriginalTweetId = originalTweetId,
CreatedAt = DateTime.UtcNow
};
await _tweetRepo.InsertTweetAsync(retweet);
await _userRepo.UpdateTweetCountAsync(userId, 1);
await _tweetRepo
.IncrementRetweetCountAsync(originalTweetId);
await _timelineCache.PushToTimelineAsync(
userId, retweet.TweetId);
await _kafka.PublishAsync("tweet-created",
new TweetCreatedEvent
{
TweetId = retweet.TweetId,
AuthorId = userId,
Content = original.Content,
IsRetweet = true,
OriginalTweetId = originalTweetId,
CreatedAt = retweet.CreatedAt
});
await _notifService.SendNotificationAsync(
new Notification
{
NotificationId = _idGen.NextId(),
Type = "retweet",
SourceUserId = userId,
TargetUserId = original.UserId,
TweetId = originalTweetId,
CreatedAt = DateTime.UtcNow
});
return retweet;
}
public async Task LikeTweetAsync(
long userId, long tweetId)
{
var tweet = await _tweetRepo.GetTweetAsync(tweetId);
if (tweet == null)
throw new ArgumentException("Tweet not found");
await _tweetRepo.IncrementLikeCountAsync(tweetId);
await _notifService.SendNotificationAsync(
new Notification
{
NotificationId = _idGen.NextId(),
Type = "like",
SourceUserId = userId,
TargetUserId = tweet.UserId,
TweetId = tweetId,
CreatedAt = DateTime.UtcNow
});
}
private static List<string> ExtractHashtags(
string content)
{
return content.Split(' ')
.Where(w => w.StartsWith('#') && w.Length > 1)
.Select(w => w.ToLowerInvariant())
.ToList();
}
private static List<string> ExtractMentions(
string content)
{
return content.Split(' ')
.Where(w => w.StartsWith('@') && w.Length > 1)
.Select(w => w[1..].ToLowerInvariant())
.ToList();
}
}
public class FollowService
{
private readonly IFollowRepository _followRepo;
private readonly IRedisClient _redis;
private readonly IKafkaProducer _kafka;
private readonly IUserRepository _userRepo;
public FollowService(
IFollowRepository followRepo,
IRedisClient redis,
IKafkaProducer kafka,
IUserRepository userRepo)
{
_followRepo = followRepo;
_redis = redis;
_kafka = kafka;
_userRepo = userRepo;
}
public async Task<bool> FollowUserAsync(
long followerId, long followeeId)
{
if (followerId == followeeId)
throw new InvalidOperationException(
"Cannot follow yourself");
var existing = await _followRepo
.GetFollowAsync(followerId, followeeId);
if (existing != null) return false;
await _followRepo.InsertFollowAsync(
followerId, followeeId);
await _userRepo
.IncrementFollowingCountAsync(followerId);
await _userRepo
.IncrementFollowerCountAsync(followeeId);
await _redis.SetAddAsync(
$"following:{followerId}",
followeeId.ToString());
await _redis.SetAddAsync(
$"followers:{followeeId}",
followerId.ToString());
await _kafka.PublishAsync("follow-events",
new FollowEvent
{
FollowerId = followerId,
FolloweeId = followeeId,
Action = FollowAction.Follow,
Timestamp = DateTime.UtcNow
});
return true;
}
public async Task<bool> UnfollowUserAsync(
long followerId, long followeeId)
{
var deleted = await _followRepo
.DeleteFollowAsync(followerId, followeeId);
if (!deleted) return false;
await _userRepo
.DecrementFollowingCountAsync(followerId);
await _userRepo
.DecrementFollowerCountAsync(followeeId);
await _redis.SetRemoveAsync(
$"following:{followerId}",
followeeId.ToString());
await _redis.SetRemoveAsync(
$"followers:{followeeId}",
followerId.ToString());
return true;
}
}
public class TimelineService
{
private readonly ITimelineCache _timelineCache;
private readonly ITweetRepository _tweetRepo;
private readonly IFollowRepository _followRepo;
private readonly ICacheClient _cache;
private const int CelebrityThreshold = 10_000;
public TimelineService(
ITimelineCache timelineCache,
ITweetRepository tweetRepo,
IFollowRepository followRepo,
ICacheClient cache)
{
_timelineCache = timelineCache;
_tweetRepo = tweetRepo;
_followRepo = followRepo;
_cache = cache;
}
public async Task<List<Tweet>> GetHomeTimelineAsync(
long userId, long? cursor = null, int limit = 30)
{
var precomputedIds = await _timelineCache
.GetTimelineAsync(userId, limit * 3);
var following = await _followRepo
.GetFollowingAsync(userId);
var celebrityIds = new List<long>();
foreach (var followeeId in following)
{
var followee = await _cache
.GetAsync<User>($"user:{followeeId}");
if (followee != null &&
followee.FollowerCount > CelebrityThreshold)
celebrityIds.Add(followeeId);
}
var celebrityTweets = new List<Tweet>();
foreach (var celebId in celebrityIds)
{
var celebTweets = await _tweetRepo
.GetUserTimelineAsync(celebId, null, 5);
celebrityTweets.AddRange(celebTweets);
}
var allTweetIds = precomputedIds
.Union(celebrityTweets.Select(t => t.TweetId))
.Distinct()
.OrderByDescending(id => id)
.Take(limit)
.ToList();
var tweets = new List<Tweet>();
foreach (var tweetId in allTweetIds)
{
var cached = await _cache.GetAsync<Tweet>(
$"tweet:{tweetId}");
if (cached != null)
{
tweets.Add(cached);
}
else
{
var fromDb = await _tweetRepo
.GetTweetAsync(tweetId);
if (fromDb != null) tweets.Add(fromDb);
}
}
return tweets.OrderByDescending(
t => t.CreatedAt).ToList();
}
public async Task HandleFanOutAsync(
long authorId, long tweetId, int followerCount)
{
if (followerCount > CelebrityThreshold)
return;
var followers = await _followRepo
.GetFollowersAsync(authorId);
var tasks = followers.Select(followerId =>
_timelineCache.PushToTimelineAsync(
followerId, tweetId));
await Task.WhenAll(tasks);
}
}
public class TrendingTopicService
{
private readonly ICacheClient _cache;
private readonly ITrendRepository _trendRepo;
public TrendingTopicService(
ICacheClient cache, ITrendRepository trendRepo)
{
_cache = cache;
_trendRepo = trendRepo;
}
public async Task<List<TrendingTopic>> GetTrendsAsync(
string regionCode, int count = 30)
{
var cacheKey = $"trends:{regionCode}";
var cached = await _cache.ListRangeAsync(cacheKey);
if (cached.Count > 0)
{
return cached
.Select(c => JsonSerializer
.Deserialize<TrendingTopic>(c))
.Where(t => t != null)
.Cast<TrendingTopic>()
.Take(count)
.ToList();
}
var trends = await _trendRepo
.GetTopTrendsAsync(regionCode, count);
foreach (var trend in trends)
{
await _cache.AddToSortedSetAsync(
cacheKey, trend, trend.VelocityScore);
}
return trends;
}
public async Task ComputeTrendsAsync()
{
var windowSize = TimeSpan.FromMinutes(5);
var currentWindow = await _trendRepo
.GetTweetCountsAsync(windowSize);
var previousWindow = await _trendRepo
.GetTweetCountsAsync(windowSize,
TimeSpan.FromMinutes(5));
var scores = new Dictionary<string, double>();
foreach (var (topic, count) in currentWindow)
{
var prevCount = previousWindow
.GetValueOrDefault(topic, 1);
var velocity = (double)count / prevCount;
var breadth = await _trendRepo
.GetUniqueUserCountAsync(topic, windowSize);
scores[topic] = count * velocity *
Math.Log2(breadth + 1);
}
var topTrends = scores
.OrderByDescending(kvp => kvp.Value)
.Take(100)
.Select(kvp => new TrendingTopic
{
Topic = kvp.Key,
TweetVolume = currentWindow[kvp.Key],
VelocityScore = kvp.Value,
ComputedAt = DateTime.UtcNow
})
.ToList();
await _trendRepo.SaveTrendsAsync(topTrends);
}
}
}C#
26. Conclusion
Designing a Twitter/X-like microblogging system at 400 million user scale is one of the most comprehensive system design challenges in the industry. It touches every aspect of distributed systems engineering, from data modeling and API design to real-time streaming, machine learning-powered ranking, and multi-region deployment.
The key takeaways from this design are: First, the hybrid fan-out approach (push for regular users, pull for celebrities) is essential for balancing write amplification against read latency. Second, polyglot persistence — using the right database for each access pattern — is critical for performance at scale. Third, aggressive caching at multiple layers (Redis for dynamic data, CDN for static content) reduces database load by orders of magnitude. Fourth, asynchronous processing via message queues (Kafka) enables the system to handle bursty traffic patterns without degrading the user experience. Fifth, the algorithmic feed ranking system must balance relevance with diversity, giving users control over their experience.
For system design interviews, the Twitter microblogging problem is an excellent vehicle for demonstrating your understanding of trade-offs. Every decision — from the sharding strategy to the caching policy to the fan-out approach — involves trade-offs between latency, throughput, consistency, and complexity. Being able to articulate these trade-offs clearly and justify your choices is what separates senior engineers from their peers.
In production, a Twitter-scale system requires a dedicated team of infrastructure engineers, SREs, and security engineers to operate reliably. The system described in this article represents the architecture at a high level, but the implementation details — failure modes, capacity planning, security hardening, and operational runbooks — require significantly more depth for production deployment.
The principles and patterns discussed in this article apply beyond microblogging. Any system that involves content feeds, social graphs, real-time data distribution, or large-scale data processing can benefit from the architectural patterns described here. Whether you are building a social media platform, a content management system, a real-time analytics dashboard, or an event-driven application, the concepts of fan-out, caching, sharding, and stream processing are universally applicable.
As you prepare for system design interviews or architect production systems, remember that the best design is not the most complex one — it is the one that makes the right trade-offs for the specific requirements and constraints of your problem. Start with the simplest design that meets the requirements, and add complexity only when the requirements demand it. This principle of progressive complexity is the hallmark of senior engineering thinking.
13. Follow/Unfollow and Social Graph
The social graph is the connective tissue of the platform. It determines which tweets appear in each user's timeline, who can see protected tweets, and how content propagates through the network.
Follow Operation
When a user follows another user, the system inserts a new row into the follows table and updates the denormalized follower_count and following_count counters on both user profiles. The system also adds the followee to the follower's social graph cache in Redis and triggers a fan-out job that adds the followee's recent tweets to the follower's home timeline cache.
Social Graph Storage
The social graph is stored in multiple representations. The primary source of truth is the follows table in MySQL. For fast access during fan-out operations, the follower lists are cached in Redis sets keyed by user_id. Redis provides O(1) membership checks and O(N) iteration for fan-out operations. For complex graph queries like finding mutual followers, a graph database like Neo4j can be used as a supplementary store.