Chapter 47: Design Instagram

Building a Photo-Sharing Platform at Scale

Interview Context

Instagram is a classic system design question that tests your ability to handle:

Time allocation: 45-60 minutes. Prioritize feed generation and image pipeline.

1. Requirements

1.1 Functional Requirements

Core Features

  • Upload photos with captions and hashtags
  • Follow/unfollow users
  • View personalized feed (following + recommended)
  • Like and comment on posts
  • Stories (24-hour ephemeral content)
  • Explore page (discover new content)

Out of Scope

  • Direct messaging
  • Video uploads (Reels)
  • Live streaming
  • Shopping features
  • Advanced editing tools

1.2 Non-Functional Requirements

Requirement Target Rationale
Availability 99.9% Social media platform, tolerate brief outages
Latency (Feed) p95 < 200ms Smooth scrolling experience
Latency (Upload) p95 < 2s User waits for confirmation
Consistency Eventual Delayed feed updates acceptable
Durability 99.9999% Photos are valuable user data

1.3 Scale Assumptions

2. Capacity Estimation

2.1 Storage Capacity

Daily Photo Storage

Original photos:    100M photos/day × 2MB = 200TB/day
Processed versions: 100M × (1MB + 500KB + 200KB + 100KB) = 180TB/day
Total daily:        380TB/day
Annual storage:     380TB × 365 = 138.7PB/year

Metadata Storage

Users:              1B users × 1KB = 1TB
Posts:              100M posts/day × 2KB = 200GB/day (metadata)
                    200GB × 365 = 73TB/year
Followers:          1B users × 200 follows × 16 bytes = 3.2TB
Total metadata:     ~75TB/year (negligible vs images)
Interview Insight: Image storage dominates. Metadata is 0.05% of total. Focus optimization on CDN delivery and multi-resolution storage.

2.2 Bandwidth Estimation

Upload Bandwidth

Photos uploaded:    100M photos/day × 2MB = 200TB/day
Peak (3× average):  200TB / 86400s × 3 = 6.9GB/s
With 4 processed:   6.9GB/s × 2.8 = 19.3GB/s (processing burst)

Download Bandwidth (Read-Heavy)

Feed views:         500M users × 30 views/day = 15B feed views
Posts per view:     20 posts (average scroll)
Images downloaded:  15B × 20 × 400KB (medium res) = 120PB/day
Peak bandwidth:     120PB / 86400s × 3 = 4.2TB/s
With CDN hit rate:  4.2TB/s × 0.15 (cache miss) = 630GB/s origin

2.3 QPS Estimation

Operation Daily Volume Avg QPS Peak QPS
Upload Photo 100M 1,157 3,500
View Feed 15B 173,611 520,000
Like Post 3B (20% engagement) 34,722 104,000
Follow User 500M 5,787 17,000
View Stories 10B 115,740 347,000
Read/Write Ratio: ~300:1 (read-heavy). Cache aggressively, optimize CDN delivery, pre-compute feeds.

3. High-Level Design

3.1 System Architecture

graph TB subgraph "Client Layer" Mobile[Mobile Apps] Web[Web Clients] end subgraph "CDN Layer" CDN[CloudFront/Cloudflare
Image Delivery] end subgraph "API Gateway" LB[Load Balancer] API[API Gateway
Rate Limiting, Auth] end subgraph "Service Layer" Upload[Upload Service] Feed[Feed Service] User[User Service] Social[Social Graph Service] Story[Story Service] Explore[Explore Service] Notif[Notification Service] end subgraph "Processing" ImgProc[Image Processing
Workers] FeedGen[Feed Generation
Workers] ML[ML Ranking Service] end subgraph "Storage Layer" S3[S3/Object Storage
Images] UserDB[(User DB
PostgreSQL)] PostDB[(Post DB
Cassandra)] GraphDB[(Graph DB
Neo4j/Custom)] Redis[(Redis
Feed Cache)] ES[Elasticsearch
Search Index] end subgraph "Messaging" Kafka[Kafka/SQS
Event Bus] end Mobile --> CDN Web --> CDN Mobile --> LB Web --> LB LB --> API API --> Upload API --> Feed API --> User API --> Social API --> Story API --> Explore Upload --> ImgProc Upload --> Kafka ImgProc --> S3 S3 --> CDN Feed --> Redis Feed --> FeedGen Feed --> ML FeedGen --> PostDB FeedGen --> GraphDB FeedGen --> Kafka Social --> GraphDB User --> UserDB Story --> Redis Story --> S3 Explore --> ES Explore --> ML Kafka --> Notif Kafka --> FeedGen Kafka --> ES style CDN fill:#e1f5ff style S3 fill:#ffe1e1 style Redis fill:#ffe1cc style Kafka fill:#f0e1ff

3.2 Component Responsibilities

Upload Service

Handles photo uploads, validates files, initiates processing pipeline, stores metadata.

Feed Service

Generates personalized feeds, ranks posts, caches results, handles feed pagination.

Social Graph Service

Manages follow/unfollow, retrieves follower lists, supports fanout operations.

Image Processing Workers

Async processing: resize, format conversion, EXIF stripping, thumbnail generation.

Feed Generation Workers

Pre-computes feeds for normal users (fanout-on-write), updates feed caches.

ML Ranking Service

Scores posts for personalized ranking, predicts engagement, filters content.

4. Image Upload Pipeline

4.1 Upload Flow

sequenceDiagram participant C as Client participant API as API Gateway participant Upload as Upload Service participant S3 as S3 Staging participant Queue as Kafka participant Worker as Image Processor participant CDN as CDN participant DB as Post DB C->>API: POST /upload (multipart) API->>Upload: Validate & initiate Upload->>Upload: Generate post_id Upload->>S3: Store original (temp) Upload->>DB: Insert metadata (processing) Upload->>Queue: PublishImageUploadEvent Upload->>C: 202 Accepted {post_id} Note over C: Client polls or websocket Queue->>Worker: ConsumeEvent Worker->>S3: Fetch original Worker->>Worker: Resize (1080p, 720p, 480p, thumb) Worker->>Worker: Strip EXIF Worker->>Worker: Optimize (WebP, AVIF) Worker->>S3: Store 4 versions Worker->>CDN: Warm cache (popular users) Worker->>DB: Update status (ready) Worker->>Queue: PublishImageReadyEvent Queue->>C: Push notification (via websocket) C->>CDN: GET /images/{post_id}/medium.webp CDN-->>C: Return image

4.2 Image Processing Details

Version Resolution Format Size Use Case
Original As uploaded Original ~2MB Archive, full quality view
Large 1080×1080 WebP/AVIF ~1MB Desktop detail view
Medium 640×640 WebP/AVIF ~400KB Mobile feed, desktop grid
Small 320×320 WebP/AVIF ~200KB Thumbnails, slow connections
Thumbnail 150×150 WebP/AVIF ~100KB Explore grid, profile grid

4.3 Upload Optimizations

Interview Tip: Discuss tradeoff between processing time vs on-demand generation. Instagram pre-processes all versions for consistency (user waits 1-2s vs unpredictable latency on first view).

5. Feed Generation

5.1 Hybrid Fanout Strategy

Instagram uses a hybrid approach combining fanout-on-write for normal users and fanout-on-read for celebrities.

Strategy When Pros Cons
Fanout-on-Write Follower count < 1M Fast read (pre-computed)
Consistent latency
Slow write for popular users
Storage overhead
Fanout-on-Read Follower count ≥ 1M Fast write
No fanout storm
Slower read (compute on demand)
Variable latency
Hybrid Instagram's approach Best of both
Handles all scales
Complexity
Merge logic required

5.2 Feed Generation Flow

sequenceDiagram participant User as User Posts participant Upload as Upload Service participant Kafka as Event Bus participant FeedGen as Feed Generator participant Graph as Social Graph participant Cache as Redis Feed Cache participant PostDB as Post DB participant Client as Feed Viewer User->>Upload: Create post Upload->>Kafka: PostCreatedEvent alt Normal User (< 1M followers) Kafka->>FeedGen: Consume event FeedGen->>Graph: GetFollowers(user_id) Graph-->>FeedGen: [follower_ids] loop Each follower FeedGen->>Cache: LPUSH feed:{follower_id} {post_id} FeedGen->>Cache: LTRIM feed:{follower_id} 0 999 end Note over FeedGen: Fanout-on-Write else Celebrity (≥ 1M followers) Kafka->>FeedGen: Mark as celebrity post FeedGen->>PostDB: Index for fanout-on-read Note over FeedGen: No fanout, lazy fetch end Client->>Cache: GET feed:{user_id} 0 19 Cache-->>Client: [post_ids from normal users] Client->>Graph: GetFollowing(user_id, celebrities) Graph-->>Client: [celebrity_ids] Client->>PostDB: GetRecentPosts(celebrity_ids) PostDB-->>Client: [celebrity posts] Client->>Client: Merge + rank Client->>Client: Render feed

5.3 Feed Cache Structure

Redis Data Structure

// Feed cache (Redis List)
Key:   feed:{user_id}
Value: [post_id:1234:ts:1715500000, post_id:1235:ts:1715499800, ...]
TTL:   3 days
Size:  Top 1000 posts (LTRIM)

// Feed metadata (Redis Hash)
Key:   feed_meta:{user_id}
Value: {last_updated: 1715500100, version: 5}
TTL:   3 days

// Celebrity posts (sorted by time)
Key:   celeb_posts:{celeb_user_id}
Type:  Sorted Set (score = timestamp)
Value: post_id
TTL:   7 days
Size:  Top 100 posts

5.4 Feed Generation Pseudo-Code

function GenerateFeed(user_id, limit=20, cursor=null):
    // 1. Fetch from cache (fanout-on-write posts)
    cached_posts = redis.LRANGE(f"feed:{user_id}", 0, 999)

    // 2. Get celebrity follows
    following = graph.GetFollowing(user_id)
    celebrities = [u for u in following if u.follower_count >= 1M]

    // 3. Fetch celebrity posts (fanout-on-read)
    celeb_posts = []
    for celeb in celebrities:
        posts = redis.ZREVRANGE(f"celeb_posts:{celeb.id}", 0, 49)
        celeb_posts.extend(posts)

    // 4. Merge and deduplicate
    all_posts = MergePosts(cached_posts, celeb_posts)

    // 5. Rank with ML model
    ranked_posts = ml_service.RankPosts(user_id, all_posts)

    // 6. Paginate
    return Paginate(ranked_posts, limit, cursor)

function FanoutOnWrite(post_id, author_id):
    // Only for non-celebrities
    author = user_service.GetUser(author_id)
    if author.follower_count >= 1M:
        // Store in celebrity index
        redis.ZADD(f"celeb_posts:{author_id}", time.now(), post_id)
        return

    // Fanout to all followers
    followers = graph.GetFollowers(author_id)
    for follower_id in followers:
        redis.LPUSH(f"feed:{follower_id}", post_id)
        redis.LTRIM(f"feed:{follower_id}", 0, 999)
        redis.EXPIRE(f"feed:{follower_id}", 3 * 86400)
Interview Insight: The 1M follower threshold is tunable. Instagram likely uses a dynamic threshold based on system load and follower engagement rate (not just count).

6. Feed Ranking

6.1 Ranking Objective

Instagram's goal: Maximize user engagement time by predicting likelihood of interaction (like, comment, share, save).

6.2 Ranking Features

Category Features Weight
Recency Post age, time since last interaction High
Relationship Interaction frequency, profile visits, DM history, mutual follows Very High
Content Type Image/video, hashtags, location, mentions Medium
Engagement Like count, comment count, share velocity, save rate High
User Preferences Historical likes, interests, dwell time on similar content Very High
Author Signals Follower count, posting frequency, verification status Low

6.3 ML Ranking Pipeline

graph LR A[Candidate Posts
~1000 posts] --> B[Feature Extraction] B --> C[Embedding Lookup
User & Post] C --> D[Lightweight Model
Filtering] D --> E[Top 200 Posts] E --> F[Heavy Model
Ranking] F --> G[Top 20 Posts
for Display] H[(Feature Store
Redis)] --> B I[(Embedding DB
Vector Store)] --> C style D fill:#ffe1cc style F fill:#ffe1cc

6.4 Two-Stage Ranking

Stage 1: Lightweight Filtering

  • Input: ~1000 candidate posts
  • Model: Logistic regression or shallow NN
  • Latency: <10ms
  • Output: Top 200 posts
  • Features: Recency, relationship score, basic engagement

Stage 2: Heavy Ranking

  • Input: 200 filtered posts
  • Model: Deep neural network (transformer)
  • Latency: <50ms
  • Output: Top 20 ranked posts
  • Features: All signals, embeddings, cross-features

6.5 Ranking Score Calculation

function RankPosts(user_id, posts):
    features = []
    for post in posts:
        f = ExtractFeatures(user_id, post)
        features.append(f)

    // Stage 1: Lightweight filter
    scores_stage1 = lightweight_model.Predict(features)
    top_200 = TopK(posts, scores_stage1, k=200)

    // Stage 2: Heavy ranking
    embeddings = embedding_service.GetEmbeddings(user_id, top_200)
    scores_stage2 = heavy_model.Predict(embeddings)

    // Apply diversity penalty
    ranked = DiversifyRanking(top_200, scores_stage2)

    return TopK(ranked, scores_stage2, k=20)

function ExtractFeatures(user_id, post):
    return {
        'recency_hours': (now - post.created_at) / 3600,
        'relationship_score': GetRelationshipScore(user_id, post.author_id),
        'like_count': post.like_count,
        'comment_count': post.comment_count,
        'engagement_velocity': post.likes_last_hour / (post.age_hours + 1),
        'user_liked_similar': UserLikedSimilarContent(user_id, post.hashtags),
        'author_follower_count': post.author.follower_count,
        'is_verified': post.author.is_verified,
        'content_type': post.type  // image, carousel, etc
    }
Cold Start Problem: New users have no interaction history. Use popularity-based ranking + onboarding interest selection + location-based content for first 2 weeks.

7. Stories Feature

7.1 Stories Characteristics

7.2 Stories Architecture

graph TB subgraph "Upload" Client[Client] -->|Upload| API[API Gateway] API --> StoryUpload[Story Upload Service] StoryUpload --> S3[S3 Story Bucket] StoryUpload --> Redis[Redis Story Index] end subgraph "Storage Strategy" S3 -->|24h lifecycle| Delete[Auto-Delete] Redis -->|TTL 86400s| Expire[Auto-Expire] end subgraph "Delivery" Viewer[Story Viewer] -->|GetStories| StoryAPI[Story Service] StoryAPI --> Redis Redis -->|story_ids| StoryAPI StoryAPI --> CDN[CDN] CDN -->|media| Viewer end subgraph "Pre-warming" FeedGen[Feed Worker] -->|Monitor| PopularUsers[Popular Users] PopularUsers -->|Push| CDN end style S3 fill:#ffe1e1 style Redis fill:#ffe1cc style CDN fill:#e1f5ff

7.3 Stories Data Model

// Redis Sorted Set (per user)
Key:   stories:{user_id}
Type:  Sorted Set
Score: timestamp (created_at)
Value: story_id
TTL:   24 hours

// Story metadata (Redis Hash)
Key:   story:{story_id}
Value: {
    user_id: 12345,
    media_url: "s3://bucket/story/xyz.jpg",
    cdn_url: "cdn.instagram.com/story/xyz.jpg",
    created_at: 1715500000,
    expires_at: 1715586400,
    view_count: 4523,
    viewers: [viewer_id_1, viewer_id_2, ...]  // capped at 1000
}
TTL:   24 hours

// Story ring status (Redis Hash)
Key:   story_ring:{viewer_id}
Value: {user_id: last_viewed_timestamp}
TTL:   24 hours

7.4 Stories Optimizations

Ring Buffer Storage

Stories stored in memory (Redis) with automatic TTL. No disk persistence needed for most users. S3 acts as backup for analytics.

CDN Pre-warming

When a user with >10K followers posts a story, immediately push to CDN edge nodes in their followers' regions.

Batched Delivery

Fetch 5 stories at once (current + next 4) to enable seamless sequential viewing without buffering.

Viewer Cap

Store max 1000 viewers per story. For celebrities, use HyperLogLog for view count (probabilistic, 1% error, 12KB memory).

7.5 Stories Feed Generation

function GetStoriesFeed(viewer_id):
    // 1. Get following list
    following = graph.GetFollowing(viewer_id)

    // 2. Check which users have active stories
    active_stories = []
    for user_id in following:
        count = redis.ZCOUNT(f"stories:{user_id}", now-86400, now)
        if count > 0:
            last_viewed = redis.HGET(f"story_ring:{viewer_id}", user_id)
            has_new = CheckNewStories(user_id, last_viewed)
            active_stories.append({
                'user_id': user_id,
                'story_count': count,
                'has_new': has_new,
                'last_story_ts': redis.ZSCORE(f"stories:{user_id}", -1)
            })

    // 3. Sort: new stories first, then by recency, then by relationship
    sorted_stories = SortStories(active_stories, viewer_id)

    return sorted_stories

function GetStoriesForUser(user_id, viewer_id):
    // Fetch all stories for user
    story_ids = redis.ZREVRANGE(f"stories:{user_id}", 0, -1)
    stories = [redis.HGETALL(f"story:{sid}") for sid in story_ids]

    // Mark as viewed
    redis.HSET(f"story_ring:{viewer_id}", user_id, time.now())

    // Increment view count
    for story_id in story_ids:
        redis.HINCRBY(f"story:{story_id}", "view_count", 1)
        redis.LPUSH(f"story:{story_id}:viewers", viewer_id)
        redis.LTRIM(f"story:{story_id}:viewers", 0, 999)

    return stories
Interview Insight: Stories are optimized for high throughput and low latency at the cost of durability. Using Redis with 24h TTL eliminates manual cleanup and enables O(1) expiration.

8. Explore Page

8.1 Explore Objectives

8.2 Explore Architecture

graph TB subgraph "Candidate Generation" Collab[Collaborative
Filtering] --> Candidates[Candidate Pool
100K posts] Content[Content-Based
Filtering] --> Candidates Trending[Trending
Detection] --> Candidates Location[Location
Based] --> Candidates end subgraph "Indexing" AllPosts[(All Public Posts)] --> Embeddings[Embedding
Generator] Embeddings --> VectorDB[(Vector DB
Pinecone/Milvus)] AllPosts --> ES[(Elasticsearch
Hashtag Index)] end subgraph "Ranking" Candidates --> Filter[Diversity Filter] Filter --> MLRank[ML Ranking
Model] MLRank --> Top[Top 50 Posts] end subgraph "Serving" User[User Request] --> Cache[(Redis
Explore Cache)] Cache -.->|miss| MLRank Top --> Cache Cache --> Display[Explore Grid] end VectorDB --> Collab VectorDB --> Content ES --> Trending style VectorDB fill:#e1ffe1 style ES fill:#fff4e1 style Cache fill:#ffe1cc

8.3 Candidate Generation Strategies

Strategy Approach Contribution
Collaborative Filtering Users similar to you liked these posts (user-user similarity) 40%
Content-Based Posts similar to what you've engaged with (post embeddings) 30%
Trending High engagement velocity in last 24 hours 20%
Location-Based Popular posts near your location 10%

8.4 Collaborative Filtering Implementation

function CollaborativeFiltering(user_id):
    // 1. Find similar users (cosine similarity on interaction vectors)
    user_embedding = embedding_service.GetUserEmbedding(user_id)
    similar_users = vector_db.SimilaritySearch(user_embedding, k=1000)

    // 2. Get posts they liked (that user hasn't seen)
    candidate_posts = set()
    for similar_user in similar_users:
        liked_posts = GetRecentLikes(similar_user.id, days=7)
        candidate_posts.update(liked_posts)

    // 3. Filter out posts from users already following
    following = graph.GetFollowing(user_id)
    candidate_posts = [p for p in candidate_posts if p.author_id not in following]

    // 4. Remove posts already seen
    seen_posts = GetSeenPosts(user_id)
    candidate_posts = [p for p in candidate_posts if p.id not in seen_posts]

    return candidate_posts[:10000]  // Top 10K by similarity score

8.5 Trending Detection

function DetectTrendingPosts():
    // Time-decayed engagement score
    // score = (likes + 2*comments + 3*shares) / (hours_since_post + 2)^1.5

    recent_posts = GetPostsInTimeWindow(hours=24)
    scored_posts = []

    for post in recent_posts:
        age_hours = (now - post.created_at) / 3600
        engagement = post.like_count + 2*post.comment_count + 3*post.share_count
        score = engagement / pow(age_hours + 2, 1.5)

        scored_posts.append({
            'post_id': post.id,
            'score': score,
            'velocity': engagement / (age_hours + 1)  // interactions per hour
        })

    // Store in Redis sorted set
    for item in scored_posts:
        redis.ZADD("trending_posts", item.score, item.post_id)

    redis.EXPIRE("trending_posts", 3600)  // Refresh every hour

    return redis.ZREVRANGE("trending_posts", 0, 999)

8.6 Explore Personalization

User Interest Profile

  • Hashtags engaged with (last 30 days)
  • Topics of liked posts (ML-classified)
  • Accounts engaged with (but not following)
  • Dwell time on different content types
  • Location history (if enabled)

Diversity Enforcement

  • Max 2 posts from same author in top 50
  • Mix of image/video/carousel formats
  • Temporal diversity (avoid all posts from same hour)
  • Topic diversity (at least 5 different topics)
  • Engagement diversity (mix of viral + niche content)
Interview Tip: Explore is computationally expensive. Pre-compute candidate pools and cache personalized explore grids for 1 hour. Refresh on user interaction (like/save) to improve relevance.

9. Database Design

9.1 Database Choices

Data Type Database Rationale
User profiles PostgreSQL Structured, ACID, infrequent updates
Posts & metadata Cassandra High write throughput, time-series data, wide-column
Social graph Neo4j / Custom Graph traversal (followers, mutual friends)
Feed cache Redis In-memory, fast reads, TTL support
Search index Elasticsearch Full-text search, hashtag lookup, trending
Images S3 Object storage, high durability, CDN integration
Embeddings Pinecone/Milvus Vector similarity search, ANN

9.2 Schema Design

User Table (PostgreSQL)

CREATE TABLE users (
    user_id BIGSERIAL PRIMARY KEY,
    username VARCHAR(30) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    full_name VARCHAR(100),
    bio TEXT,
    profile_pic_url VARCHAR(500),
    follower_count INT DEFAULT 0,
    following_count INT DEFAULT 0,
    post_count INT DEFAULT 0,
    is_verified BOOLEAN DEFAULT FALSE,
    is_private BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);

Posts Table (Cassandra)

CREATE TABLE posts (
    post_id UUID PRIMARY KEY,
    user_id BIGINT,
    caption TEXT,
    image_urls LIST,  // [original, large, medium, small, thumb]
    hashtags SET,
    mentions SET,
    location TEXT,
    like_count COUNTER,
    comment_count COUNTER,
    share_count COUNTER,
    save_count COUNTER,
    created_at TIMESTAMP,
    updated_at TIMESTAMP
) WITH CLUSTERING ORDER BY (created_at DESC);

// User's posts (timeline)
CREATE TABLE posts_by_user (
    user_id BIGINT,
    post_id UUID,
    created_at TIMESTAMP,
    PRIMARY KEY (user_id, created_at, post_id)
) WITH CLUSTERING ORDER BY (created_at DESC);

// Hashtag index
CREATE TABLE posts_by_hashtag (
    hashtag TEXT,
    post_id UUID,
    created_at TIMESTAMP,
    PRIMARY KEY (hashtag, created_at, post_id)
) WITH CLUSTERING ORDER BY (created_at DESC);

Social Graph (Custom/Neo4j)

// Adjacency list representation (optimized for reads)

// Followers: who follows user X
CREATE TABLE followers (
    user_id BIGINT,
    follower_id BIGINT,
    created_at TIMESTAMP,
    PRIMARY KEY (user_id, follower_id)
);

// Following: who does user X follow
CREATE TABLE following (
    user_id BIGINT,
    following_id BIGINT,
    created_at TIMESTAMP,
    PRIMARY KEY (user_id, following_id)
);

// Both tables denormalized for fast lookups
// Trade-off: 2× storage, but O(1) lookups in both directions

CREATE INDEX idx_followers_follower ON followers(follower_id);
CREATE INDEX idx_following_following ON following(following_id);

Likes Table (Cassandra)

CREATE TABLE likes (
    post_id UUID,
    user_id BIGINT,
    created_at TIMESTAMP,
    PRIMARY KEY (post_id, user_id)
);

CREATE TABLE likes_by_user (
    user_id BIGINT,
    post_id UUID,
    created_at TIMESTAMP,
    PRIMARY KEY (user_id, created_at, post_id)
) WITH CLUSTERING ORDER BY (created_at DESC);

9.3 Sharding Strategy

Shard by user_id

Hash function: shard = user_id % num_shards

  • Users table: Shard by user_id
  • Posts: Shard by user_id (author_id) for write locality
  • Followers/Following: Shard by user_id (the person being followed/following)
  • Feed cache: Shard by viewer_id (the person viewing feed)

Pros: Simple, predictable, good write locality

Cons: Hot shards for celebrities, requires scatter-gather for cross-user queries

Interview Insight: Cassandra's partition key should be the query pattern. For posts_by_user, partition by user_id. For feed reads, partition by viewer_id (not author_id).

10. Scalability

10.1 Caching Layers

Layer Technology TTL Hit Rate
CDN (images) CloudFront/Cloudflare 30 days 95%
Feed cache Redis 3 days 85%
User profile Redis 1 hour 90%
Post metadata Redis 15 minutes 80%
Social graph Redis 1 hour 95%
Explore page Redis 1 hour 70%

10.2 Horizontal Scaling

API Servers

Stateless servers behind load balancer. Auto-scale based on CPU/QPS. Target: 10K QPS per server.

Image Processing Workers

Kafka consumer groups. Scale based on queue depth. Target: 100 images/sec per worker.

Feed Generation Workers

Background jobs. Scale based on fanout lag. Target: 1M feed updates/sec (100 workers).

Database Shards

Start with 64 shards, scale to 1024. Add shards via consistent hashing with virtual nodes.

10.3 Async Processing

Offload non-critical writes to Kafka for async processing:

10.4 Database Scaling

Read Replicas (PostgreSQL)

Primary for writes, 5 read replicas per shard. Route read traffic via load balancer. Tolerate replication lag up to 1 second.

Cassandra Scaling

Replication factor 3, quorum reads/writes. Add nodes dynamically, data rebalances automatically. Use consistent hashing for even distribution.

Redis Cluster

16 shards, 3 replicas each (48 nodes total). Client-side sharding via Redis Cluster protocol. Failover handled by Sentinel.

10.5 Hot Shard Mitigation

  • Celebrity detection: Flag users with >1M followers, route to dedicated shards with higher capacity
  • Cache warming: Pre-load celebrity posts into CDN before fanout completes
  • Rate limiting: Limit celebrity posting frequency to 10 posts/hour to prevent fanout storms
  • Dedicated fanout workers: Separate queue for celebrity posts, higher worker count
  • Fanout-on-read: Skip fanout entirely for mega-celebrities (>10M followers)
Bottleneck Alert: Social graph reads are the primary bottleneck. Cache follower lists aggressively. For celebrities, cache follower IDs in Redis (sorted set) with 24h TTL.

11. Tradeoffs

11.1 Fanout Strategy

Decision Choice Rationale Tradeoff
Fanout model Hybrid (write + read) Balance write cost for celebrities vs read latency for normal users Increased complexity, merge logic, dual code paths
Celebrity threshold 1M followers Fanout cost exceeds 1M write ops, Redis capacity limit Arbitrary cutoff, some celebrities still get fanout-on-write
Feed cache size 1000 posts per user Covers 99% of users scrolling depth, ~100KB memory per user Heavy scrollers hit database, 100GB Redis for 1M cached users

11.2 Consistency vs Latency

Feature Consistency Model Impact
Feed generation Eventual (up to 30s delay) New posts don't appear immediately in follower feeds. Users tolerate this.
Like count Eventual (counters) Like count may be stale by seconds. Acceptable for social media.
Follow/unfollow Strong (ACID) User expects immediate effect. Use synchronous write to graph DB.
Post upload Strong (metadata), Eventual (fanout) Post visible to author immediately, appears in feeds eventually.

11.3 Push vs Pull Notifications

Push Notifications

Use cases: Likes, comments, new followers

  • Pros: Real-time, low latency, user engagement
  • Cons: Spam potential, battery drain, infrastructure cost

Implementation: Firebase Cloud Messaging (FCM), aggregate similar notifications (5 likes → "X and 4 others liked your post")

Pull Notifications

Use cases: Activity feed, notification badge count

  • Pros: Lower cost, user controls frequency
  • Cons: Delayed delivery, requires polling

Implementation: Client polls /notifications endpoint every 30s when app is active, checks Redis counter for unread count

11.4 Storage Optimization

Decision Choice Savings Tradeoff
Image formats WebP/AVIF over JPEG 30-50% size reduction Encoding CPU cost, compatibility issues (old browsers)
EXIF stripping Remove metadata 10-20% size reduction Lose camera info, geolocation (privacy win)
Stories storage Lower replication (RF=2 vs RF=3) 33% storage reduction Lower durability (99.9% vs 99.99%), acceptable for ephemeral
Old post archival Glacier after 2 years 80% cost reduction Slow retrieval (hours), rarely accessed posts

11.5 Interview Discussion Points

Why not fanout-on-write for everyone?

A celebrity with 50M followers posting once creates 50M write operations. At 1ms per write, that's 13.8 hours of sequential writes. With parallelization (1000 workers), still 50 seconds. Fanout-on-read retrieves celebrity posts in 10ms on-demand.

Why pre-compute feeds at all?

For 500M users viewing feeds 30 times/day, that's 15B feed generations. Even at 10ms each, that's 1.5M core-hours/day. Pre-computing amortizes cost: one fanout (100ms) serves 30 reads (3ms total).

Why eventual consistency for likes?

Likes arrive at 34K QPS peak. Strong consistency would require distributed transactions across shards. Cassandra counters provide linearizable increments with eventual convergence, acceptable for social metrics.

Why multiple image resolutions?

Mobile 4G users on feed: 400KB medium image loads in 0.8s. 2MB original would take 4s. Desktop detail view needs 1080p quality. Responsive design requires multiple resolutions for optimal UX.

Summary

Key Insights

  • Hybrid fanout handles scale: write for normal, read for celebrities
  • Multi-resolution images optimize delivery for different contexts
  • Read-heavy workload demands aggressive caching (CDN, Redis, feed pre-compute)
  • Stories leverage Redis TTL for automatic expiration
  • ML ranking balances recency, relationship, and engagement signals

Bottlenecks

  • Social graph reads (follower lookups for fanout)
  • Celebrity post fanout storms
  • Image processing pipeline capacity
  • Feed cache memory (100GB for 1M users × 1000 posts)
  • ML ranking latency at scale

Operational Readiness (Production-Grade)

Service Level Indicators (SLIs)

Monitoring & Alerting

Deployment Strategy

Circuit Breakers

Disaster Recovery