Skip to content
advancedPhase 52 · HLD Case Studies

Instagram Feed

Design Instagram's photo sharing and feed generation system.

2h
0 problems
Topic Progress0%

Requirements & Scope

Functional Requirements

Feature Description
Upload Photos/Videos Users can upload images (JPEG, PNG, HEIC) and videos (MP4, MOV) up to 60 minutes
View Feed Users see a feed of posts from people they follow, ranked by relevance
Follow/Unfollow Users can follow other accounts to see their content
Like & Comment Users can interact with posts via likes and threaded comments
Stories Ephemeral photos/videos visible for 24 hours, shown at the top of the feed
Explore Discover trending and personalized content beyond who you follow
Direct Messages 1:1 and group messaging with media sharing
Notifications Push notifications for likes, comments, follows, and mentions

Non-Functional Requirements

Requirement Target
Fast Image Upload Upload should complete within 2-3 seconds for the user
Low-Latency Feed Feed loads in under 500ms (p99)
High Availability 99.99% uptime (52 minutes downtime/year)
Image Durability 99.999999999% (11 nines) — never lose a photo
Read-Heavy Workload 90% reads, 10% writes
Scale 2 billion monthly active users, 100 million photos uploaded per day

Capacity Estimation

Traffic per day:
  DAU: ~500 million
  Feed requests: 500M × 10 feeds/day = 5B feed requests/day
  Uploads: 100M photos/day = ~1,160 uploads/sec
  Storage: 100M photos × 2MB avg = 200TB/day
  CDN traffic: 5B feed views × 2MB = 10 EB/day (served from CDN)

Storage (5 years):
  200TB/day × 365 × 5 = 365 PB raw
  With replicas (3x): ~1.1 EB

Database sizing:
  User table: 2B rows × 1KB = 2 TB
  Post metadata: 365B posts × 0.5KB = 182 TB
  Fanout table: massive — needs Cassandra or similar

Media Storage & Upload Pipeline

Upload Flow

┌─────────┐     ┌──────────────┐     ┌─────────────┐     ┌──────────────────┐     ┌─────────┐
│ Client  │────▶│ Upload       │────▶│ S3 (Raw)    │────▶│ Image            │────▶│ S3      │
│         │     │ Service      │     │             │     │ Processor        │     │(Resized)│
└─────────┘     └──────────────┘     └─────────────┘     └──────────────────┘     └─────────┘
      │                                    │                      │                      │
      │  Signed URL                        │                      │  Multiple sizes      │
      │◀───────────────────────────────────│                      │  ── Thumbnail (150x) │
      │                                    │                      │  ── Medium (640x)    │
      │                                    │                      │  ── Full (1080x)     │
      │                                    │                      │  ── Original         │
      │                                    │                      │                      │
      │                                    ▼                      ▼                      ▼
      │                              ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
      │                              │ Metadata     │     │ CDN (Cloud-  │     │ CDN Edge     │
      │                              │ Service      │     │ front)       │     │ Servers      │
      │                              └──────────────┘     └──────────────┘     └──────────────┘
      │                                    │
      │                                    ▼
      │                              ┌──────────────┐
      │                              │ PostgreSQL   │
      │                              │ (User, Post  │
      │                              │  Metadata)   │
      │                              └──────────────┘
      │
      │  Post ID
      ▼

Detailed Upload Pipeline

Step 1: Client Request

# Client requests a pre-signed S3 URL
POST /api/v1/upload/request
Body: { "content_type": "image/jpeg", "file_size": 2048000 }

# Response
{
    "upload_url": "https://s3.amazonaws.com/bucket/prefix?X-Amz-Signature=...",
    "upload_id": "uuid-v4",
    "expires_in": 300
}

Step 2: Direct S3 Upload

PUT https://s3.amazonaws.com/bucket/prefix?X-Amz-Signature=...
Content-Type: image/jpeg

<binary data>

# S3 returns 200 OK, triggers SQS event

Step 3: Async Image Processing (SQS + Workers)

# Worker picks up message from SQS
import boto3
from PIL import Image
import io

def process_image(s3_bucket, s3_key, upload_id):
    # Download original from S3
    s3 = boto3.client('s3')
    original = s3.get_object(Bucket=s3_bucket, Key=s3_key)['Body'].read()

    # Generate multiple resolutions
    sizes = {
        'thumbnail': (150, 150),
        'medium': (640, 640),
        'full': (1080, 1080),
    }

    for size_name, dimensions in sizes.items():
        img = Image.open(io.BytesIO(original))
        img.thumbnail(dimensions, Image.LANCZOS)

        buffer = io.BytesIO()
        img.save(buffer, format='JPEG', quality=85, optimize=True)
        buffer.seek(0)

        output_key = f"{upload_id}/{size_name}.jpg"
        s3.put_object(
            Bucket=s3_bucket,
            Key=output_key,
            Body=buffer,
            ContentType='image/jpeg',
            CacheControl='max-age=31536000',  # 1 year
            Metadata={'upload_id': upload_id}
        )

    # Store metadata in database
    store_metadata(upload_id, {
        'thumbnail_url': f"https://cdn.example.com/{upload_id}/thumbnail.jpg",
        'medium_url': f"https://cdn.example.com/{upload_id}/medium.jpg",
        'full_url': f"https://cdn.example.com/{upload_id}/full.jpg",
        'original_url': f"https://s3.amazonaws.com/{s3_bucket}/{s3_key}",
        'width': img.width,
        'height': img.height,
        'processed_at': datetime.utcnow()
    })

Image Storage Strategy

Resolution Dimensions Use Case Storage
Thumbnail 150×150 px Feed preview, grid view S3 + CDN
Medium 640×640 px Feed full view S3 + CDN
Full 1080×1080 px Detail view, zoom S3 + CDN
Original Full resolution Download, re-edit S3 (rare access)

Video Processing

Videos require a more complex pipeline:

Video Upload → S3 → SQS → MediaConvert (AWS) or FFmpeg Workers
    ├── Transcode to H.264 (MP4)
    ├── Generate HLS manifest (adaptive bitrate)
    ├── Extract thumbnail at 0s, 25%, 50%, 75%
    ├── Generate preview GIF (5s loop)
    ├── Upload to S3 + CloudFront
    └── Store metadata in DB

CDN Configuration

# CloudFront Distribution
distribution:
  origins:
    - domain: s3-bucket.s3.amazonaws.com
      path: /media
      cache_behavior:
        default_ttl: 86400        # 24 hours
        max_ttl: 31536000         # 1 year
        min_ttl: 0
        allowed_methods: [GET, HEAD]
        cached_methods: [GET, HEAD]
        compress: true
  behaviors:
    - path_pattern: "/media/*/thumbnail.*"
      default_ttl: 604800         # 7 days (thumbnails rarely change)
    - path_pattern: "/media/*/full.*"
      default_ttl: 86400          # 24 hours
    - path_pattern: "/media/original/*"
      default_ttl: 0              # No cache (direct S3)

Feed Generation & Ranking

Architecture Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                          FEED GENERATION SYSTEM                        │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────┐  │
│  │ User     │───▶│ Fanout       │───▶│ Feed Cache   │───▶│ Feed     │  │
│  │ Posts    │    │ Service      │    │ (Redis)      │    │ Service  │  │
│  └──────────┘    └──────────────┘    └──────────────┘    └──────────┘  │
│       │                │                                       │       │
│       │                ▼                                       ▼       │
│       │         ┌──────────────┐                         ┌──────────┐  │
│       │         │ Fanout       │                         │ User     │  │
│       │         │ Workers      │                         │ Timeline │  │
│       │         └──────────────┘                         │ (Sorted │  │
│       │                │                                  │  Sets)   │  │
│       │                ▼                                  └──────────┘  │
│       │         ┌──────────────┐                                      │
│       └────────▶│ Cassandra    │  (Feed data — partitioned by user_id) │
│                 └──────────────┘                                      │
└─────────────────────────────────────────────────────────────────────────┘

Fanout-on-Write Strategy

When a user posts, we immediately push that post to all their followers' feeds.

class FeedFanoutService:
    def __init__(self):
        self.redis = RedisCluster()
        self.cassandra = CassandraClient()
        self.sqs = boto3.client('sqs')

    def fanout_post(self, post: Post):
        """Push post to all followers' feed caches."""
        author_id = post.author_id
        followers = self.get_followers(author_id)

        # Celebrity threshold: accounts with > 10K followers
        if len(followers) > 10_000:
            # For celebrities, use fanout-on-read instead
            self.mark_celebrity_post(post)
            return

        # Normal users: fanout to all followers
        batch_size = 100
        for i in range(0, len(followers), batch_size):
            batch = followers[i:i + batch_size]
            self.sqs.send_message(
                QueueUrl=self.fanout_queue_url,
                MessageBody=json.dumps({
                    'post_id': post.id,
                    'follower_ids': batch,
                    'timestamp': post.created_at
                })
            )

    def process_fanout(self, message):
        """Worker processes fanout message."""
        data = json.loads(message.body)
        post_id = data['post_id']
        timestamp = data['timestamp']

        for follower_id in data['follower_ids']:
            # Add to Redis sorted set (feed cache)
            pipe = self.redis.pipeline()
            pipe.zadd(
                f"feed:{follower_id}",
                {post_id: timestamp}
            )
            pipe.zremrangebyrank(f"feed:{follower_id}", 0, -2001)  # Keep last 2000
            pipe.expire(f"feed:{follower_id}", 86400 * 7)  # 7 day TTL
            pipe.execute()

            # Also persist to Cassandra for durability
            self.cassandra.execute("""
                INSERT INTO user_feed (user_id, post_id, post_time)
                VALUES (%s, %s, %s)
            """, (follower_id, post_id, timestamp))

Fanout-on-Read Strategy (Celebrity Fallback)

class FeedService:
    def get_feed(self, user_id: str, limit: int = 20) -> List[Post]:
        """Get feed for a user. Hybrid: cached + on-read."""
        # Step 1: Get cached feed (fanout-on-write posts)
        cached_post_ids = self.redis.zrevrange(
            f"feed:{user_id}", 0, limit * 2
        )

        # Step 2: Fetch posts from followed celebrities (fanout-on-read)
        celebrity_posts = self.get_celebrity_posts(user_id, limit=50)

        # Step 3: Merge and deduplicate
        all_post_ids = set(cached_post_ids)
        all_post_ids.update([p.id for p in celebrity_posts])

        # Step 4: Rank the merged feed
        posts = self.get_posts_by_ids(list(all_post_ids))
        ranked = self.rank_feed(posts, user_id)

        return ranked[:limit]

    def get_celebrity_posts(self, user_id: str, limit: int) -> List[Post]:
        """Get recent posts from celebrities this user follows."""
        celebrities = self.get_followed_celebrities(user_id)
        posts = []

        for celeb_id in celebrities:
            # Get recent posts directly from DB
            recent = self.post_repository.get_recent(
                author_id=celeb_id,
                limit=5
            )
            posts.extend(recent)

        return sorted(posts, key=lambda p: p.created_at, reverse=True)[:limit]

Feed Ranking Algorithm

class FeedRanker:
    """Hybrid ranking: chronological + engagement signals."""

    def rank_feed(self, posts: List[Post], user_id: str) -> List[Post]:
        user_preferences = self.get_user_preferences(user_id)

        scored = []
        for post in posts:
            score = self.calculate_score(post, user_id, user_preferences)
            scored.append((post, score))

        # Sort by score descending
        scored.sort(key=lambda x: x[1], reverse=True)
        return [post for post, score in scored]

    def calculate_score(self, post, user_id, prefs):
        # Time decay: posts lose relevance over time
        age_hours = (datetime.utcnow() - post.created_at).total_seconds() / 3600
        time_score = 1.0 / (1.0 + age_hours * 0.1)  # Half-life ~10 hours

        # Engagement signals
        engagement = (
            post.likes_count * 1.0 +
            post.comments_count * 2.0 +
            post.shares_count * 3.0
        )
        engagement_score = math.log1p(engagement) / 10.0

        # Affinity: how close is this user to the author?
        affinity = self.get_affinity_score(user_id, post.author_id)

        # Content type preference
        type_prefs = {
            'photo': prefs.get('photo_weight', 1.0),
            'video': prefs.get('video_weight', 1.2),
            'carousel': prefs.get('carousel_weight', 1.1),
        }
        type_score = type_prefs.get(post.content_type, 1.0)

        # Final weighted score
        return (
            0.3 * time_score +
            0.3 * engagement_score +
            0.25 * affinity +
            0.15 * type_score
        )

    def get_affinity_score(self, user_id, author_id):
        """Score based on past interactions with this author."""
        interactions = self.get_interactions(user_id, author_id)
        return min(1.0, interactions / 100.0)  # Normalize to [0, 1]

Database Schema (Cassandra)

-- Feed table: partition by user_id, cluster by post_time DESC
CREATE TABLE user_feed (
    user_id UUID,
    post_id UUID,
    post_time TIMESTAMP,
    author_id UUID,
    PRIMARY KEY (user_id, post_time)
) WITH CLUSTERING ORDER BY (post_time DESC)
  AND default_time_to_live = 604800;  -- 7 days TTL

-- Post table: partition by post_id
CREATE TABLE posts (
    post_id UUID,
    author_id UUID,
    content_type TEXT,
    caption TEXT,
    media_urls MAP<TEXT, TEXT>,  -- {'thumbnail': '...', 'full': '...'}
    likes_count COUNTER,
    comments_count COUNTER,
    created_at TIMESTAMP,
    PRIMARY KEY (post_id)
);

-- Fanout table: track which posts were fanned out to whom
CREATE TABLE feed_fanout (
    user_id UUID,
    post_id UUID,
    fanned_at TIMESTAMP,
    PRIMARY KEY (user_id, post_id)
) WITH default_time_to_live = 604800;

Caching Strategy

Multi-Layer Caching Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                        CACHING HIERARCHY                           │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Layer 1: Client Cache                                              │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  iOS/Android: NSUserDefaults / SharedPreferences            │   │
│  │  - Last 50 feed items                                       │   │
│  │  - User profile (24h TTL)                                    │   │
│  │  - Image thumbnails (in-memory LRU, 50MB limit)             │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  Layer 2: CDN Cache (CloudFront)                                    │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  - All image/video assets                                    │   │
│  │  - 24h default TTL, 1 year max TTL                          │   │
│  │  - Edge locations: 200+ PoPs worldwide                       │   │
│  │  - Cache hit ratio target: > 95%                             │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  Layer 3: Application Cache (Redis Cluster)                         │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  - Feed cache: sorted sets (user_id → post_ids by time)     │   │
│  │  - User profiles: hash maps                                  │   │
│  │  - Session data: auth tokens                                 │   │
│  │  - Rate limiting counters                                    │   │
│  │  - Sharded across 100+ nodes, each node: 256GB RAM           │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│                              ▼                                      │
│  Layer 4: Database (Cassandra / PostgreSQL)                          │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  - Source of truth for all data                              │   │
│  │  - Read replicas for heavy read queries                      │   │
│  │  - Cassandra: feed data, posts, fanout                       │   │
│  │  - PostgreSQL: user accounts, settings, relationships        │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

Redis Feed Cache Implementation

class FeedCache:
    """Redis-based feed cache with smart eviction."""

    def __init__(self):
        self.redis = RedisCluster()
        self.FEED_TTL = 86400 * 7  # 7 days
        self.MAX_FEED_SIZE = 2000  # Max posts per user feed

    def add_to_feed(self, user_id: str, post_id: str, score: float):
        """Add post to user's feed cache."""
        key = f"feed:{user_id}"
        pipe = self.redis.pipeline()

        # Add to sorted set (score = timestamp for chronological)
        pipe.zadd(key, {post_id: score})

        # Trim to keep only latest N posts
        pipe.zremrangebyrank(key, 0, -(self.MAX_FEED_SIZE + 1))

        # Set TTL
        pipe.expire(key, self.FEED_TTL)

        pipe.execute()

    def get_feed(self, user_id: str, offset: int = 0, limit: int = 20) -> List[str]:
        """Get paginated feed from cache."""
        key = f"feed:{user_id}"

        # Get post IDs in reverse chronological order
        post_ids = self.redis.zrevrange(key, offset, offset + limit - 1)

        if not post_ids:
            return []  # Cache miss — need to rebuild from Cassandra

        return post_ids

    def rebuild_feed(self, user_id: str):
        """Rebuild feed cache from Cassandra (on cache miss)."""
        # Get recent posts from fanout table
        rows = cassandra.execute("""
            SELECT post_id, post_time FROM user_feed
            WHERE user_id = %s
            LIMIT %s
        """, (user_id, self.MAX_FEED_SIZE))

        pipe = self.redis.pipeline()
        key = f"feed:{user_id}"

        for row in rows:
            pipe.zadd(key, {str(row.post_id): row.post_time.timestamp()})

        pipe.expire(key, self.FEED_TTL)
        pipe.execute()

Cache Invalidation Strategy

Event Invalidation Action
New post created Fanout to followers' feed caches
Post deleted Remove from all followers' caches + mark soft-delete in DB
User follows someone Add that user's recent posts to follower's cache
User unfollows Remove that user's posts from feed cache
Post edited Update metadata in DB (media URLs don't change)
Content flagged Remove from all caches, mark as removed in DB

CDN Caching Headers

# When uploading media to S3, set proper cache headers
def upload_to_s3_with_caching(upload_id, image_data, size_type):
    s3.put_object(
        Bucket=BUCKET,
        Key=f"media/{upload_id}/{size_type}.jpg",
        Body=image_data,
        ContentType='image/jpeg',
        CacheControl='public, max-age=31536000, immutable',
        # immutable = browser won't revalidate (content-addressable URL)
        Metadata={
            'Content-Hash': hashlib.sha256(image_data).hexdigest()
        }
    )

# CDN invalidation for emergency cases (costs $0.005/invalidation)
def invalidate_cdn(paths: List[str]):
    cloudfront.create_invalidation(
        DistributionId=DISTRIBUTION_ID,
        Items=[{'Path': p, 'Quantity': len(paths)}],
        InvalidationBatch={
            'Paths': {'Items': paths, 'Quantity': len(paths)},
            'CallerReference': str(time.time())
        }
    )

Stories Implementation

class StoriesService:
    """Ephemeral stories with 24-hour TTL."""

    def __init__(self):
        self.redis = RedisCluster()
        self.STORY_TTL = 86400  # 24 hours in seconds

    def post_story(self, user_id: str, media_url: str) -> str:
        story_id = str(uuid.uuid4())
        story_data = {
            'id': story_id,
            'user_id': user_id,
            'media_url': media_url,
            'created_at': time.time(),
            'viewers': []  # List of user_ids who viewed
        }

        # Store in Redis with TTL
        pipe = self.redis.pipeline()
        pipe.setex(
            f"story:{story_id}",
            self.STORY_TTL,
            json.dumps(story_data)
        )

        # Add to user's story ring
        pipe.zadd(
            f"stories:{user_id}",
            {story_id: time.time()}
        )
        pipe.expire(f"stories:{user_id}", self.STORY_TTL)

        pipe.execute()
        return story_id

    def get_stories(self, user_id: str) -> List[dict]:
        """Get stories from followed users."""
        following = self.get_following(user_id)
        stories = []

        for followee_id in following:
            story_ids = self.redis.zrevrange(
                f"stories:{followee_id}", 0, 9  # Latest 10
            )
            for sid in story_ids:
                data = self.redis.get(f"story:{sid}")
                if data:  # Still exists (not expired)
                    story = json.loads(data)
                    if user_id not in story['viewers']:
                        stories.append(story)

        # Sort by recency
        stories.sort(key=lambda s: s['created_at'], reverse=True)
        return stories

Performance Metrics

Metric Target Current
Feed load latency (p50) < 100ms 85ms
Feed load latency (p99) < 500ms 380ms
Image upload latency < 2s 1.5s
CDN cache hit ratio > 95% 97.2%
Redis cache hit ratio > 90% 93.8%
Feed rebuild time (cold) < 5s 3.2s
Story view latency < 200ms 120ms

System Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│                            INSTAGRAM ARCHITECTURE                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌──────────┐                                                              │
│  │  Mobile  │──┐                                                           │
│  │  Client  │  │                                                           │
│  └──────────┘  │    ┌──────────────┐    ┌──────────────┐                  │
│                ├───▶│  API Gateway │───▶│  Load        │                  │
│  ┌──────────┐  │    │  (Kong/AWS)  │    │  Balancer    │                  │
│  │  Web     │──┘    └──────────────┘    └──────┬───────┘                  │
│  │  Client  │                                  │                           │
│  └──────────┘                    ┌─────────────┼─────────────┐            │
│                                  │             │             │            │
│                                  ▼             ▼             ▼            │
│                           ┌───────────┐ ┌───────────┐ ┌───────────┐     │
│                           │  Upload   │ │   Feed    │ │  Stories  │     │
│                           │  Service  │ │  Service  │ │  Service  │     │
│                           └─────┬─────┘ └─────┬─────┘ └─────┬─────┘     │
│                                 │             │             │             │
│                           ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐     │
│                           │    SQS    │ │   Redis   │ │   Redis   │     │
│                           └─────┬─────┘ └─────┬─────┘ └─────┬─────┘     │
│                                 │             │             │             │
│                           ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐     │
│                           │  Image    │ │Cassandra  │ │ Cassandra │     │
│                           │ Processor │ │           │ │           │     │
│                           └─────┬─────┘ └───────────┘ └───────────┘     │
│                                 │                                        │
│                           ┌─────▼─────┐                                  │
│                           │    S3     │                                  │
│                           │  (Media)  │                                  │
│                           └─────┬─────┘                                  │
│                                 │                                        │
│                           ┌─────▼─────┐                                  │
│                           │ CloudFront│                                  │
│                           │   (CDN)   │                                  │
│                           └───────────┘                                  │
└─────────────────────────────────────────────────────────────────────────────┘

Practice Problems

0/3solved
Design Instagram Feed (Design Instagram) System

Design a scalable Instagram Feed (Design Instagram) system. Cover high-level architecture, data model, and API design.

Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliability
Instagram Feed (Design Instagram) Scaling

How would you scale Instagram Feed (Design Instagram) to handle 10x the current load? Identify bottlenecks and solutions.

Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decomposition
Instagram Feed (Design Instagram) Failure Modes

Analyze potential failure modes for Instagram Feed (Design Instagram) and design mitigation strategies.

Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradation

Quiz

1. Why does Instagram use fanout-on-write for normal users but fanout-on-read for celebrities?

Question 1 options

2. What is the primary purpose of generating multiple image resolutions during upload?

Question 2 options

3. Why is Cassandra preferred over PostgreSQL for the feed data table?

Question 3 options

4. How does the system ensure images are never lost (11 nines durability)?

Question 4 options

5. What is the benefit of using a signed URL for direct client-to-S3 uploads?

Question 5 options

6. How do Stories handle expiration without expensive database scans?

Question 6 options

Flashcards

Question

What is fanout-on-write vs fanout-on-read?

Answer

Fanout-on-write: when a user posts, push the post ID to all followers' feed caches immediately. Fanout-on-read: when a user requests their feed, fetch recent posts from followed users at read time. Instagram uses a hybrid — fanout-on-write for normal users (<10K followers) and fanout-on-read for celebrities.

Question

Why generate multiple image resolutions?

Answer

Thumbnail (150px) for grid views, Medium (640px) for feed, Full (1080px) for detail view, Original for download. This optimizes bandwidth, load times, and CDN cache efficiency by serving only the needed size.

Question

What is the role of S3 pre-signed URLs in the upload flow?

Answer

Pre-signed URLs allow clients to upload directly to S3 without proxying through the app server. The server generates a time-limited signed URL with specific permissions (PUT, content-type, size limits), then the client uploads directly. Reduces server load and bandwidth.

Question

Why use Cassandra for feed data instead of PostgreSQL?

Answer

Cassandra excels at high write throughput (fanout writes), partition-based data model (partition by user_id), and time-series queries (get feed ordered by time). PostgreSQL would bottleneck on write volume and doesn't partition as naturally for this access pattern.

Question

How does Stories expiration work without database scans?

Answer

Stories are stored in Redis with a 24-hour TTL (time-to-live). Redis automatically deletes expired keys at the OS level — no cron jobs or scans needed. This makes TTL-based expiration efficient for ephemeral content.

Question

What is the feed ranking formula concept?

Answer

Feed ranking combines: Time Decay (recent posts score higher), Engagement Signals (likes, comments, shares), Affinity Score (how often user interacts with the author), and Content Type Preference (video vs photo). Weighted combination determines final rank.

Question

How does CDN caching reduce latency for images?

Answer

CloudFront caches images at 200+ edge locations worldwide. When a user requests an image, it's served from the nearest edge (typically <50ms), not from origin S3 (potentially >200ms). Cache hit ratio target is >95%.

Question

What happens during a cache miss for feed data?

Answer

When Redis cache is empty for a user's feed, the system rebuilds from Cassandra (source of truth), populates Redis with the results, and returns to the user. This is called cache-aside or lazy loading.

Revision Notes

Key Takeaways

  • 1.Instagram is a read-heavy system (90:10 read:write ratio) — optimize reads with caching at every layer
  • 2.The hybrid fanout approach (on-write for normal, on-read for celebrities) balances write amplification with read latency
  • 3.Media processing should be fully async — clients upload to S3 directly, processing happens in background workers
  • 4.Multiple image resolutions serve different UI contexts efficiently without wasting bandwidth
  • 5.Redis TTL is the cleanest way to handle ephemeral content like Stories
  • 6.CDN is critical — most image traffic never reaches origin servers
  • 7.Feed ranking is a balance of recency, engagement, and user affinity

Interview Tips

  • Start with requirements: clarify functional vs non-functional, estimate scale numbers early
  • Draw the high-level architecture first, then dive into each component
  • Always discuss the tradeoff between fanout-on-write (write amplification) and fanout-on-read (read latency)
  • Mention CDN strategy — interviewers expect this for any media-heavy system
  • Be ready to discuss feed ranking algorithms — it shows depth beyond basic CRUD
  • Address Stories as a separate service with TTL-based storage — shows you think about ephemeral data
  • Discuss failure modes: what happens when Redis is down? When S3 is unreachable?
  • Quantify everything: latency targets, cache hit ratios, storage estimates

Cheat Sheet

Instagram System Design Cheat Sheet

Scale Numbers

  • 2B MAU, 500M DAU
  • 100M photos/day → ~1,160 uploads/sec
  • 200TB new storage/day
  • 5B feed requests/day
  • 90% reads, 10% writes

Core Architecture

Client → API Gateway → Load Balancer
    ├── Upload Service → SQS → Image Processor → S3 → CDN
    ├── Feed Service → Redis (cache) → Cassandra (storage)
    ├── Stories Service → Redis (TTL=24h)
    └── Notification Service → Push (APNS/FCM)

Upload Pipeline

  1. Client requests pre-signed S3 URL
  2. Client uploads directly to S3
  3. S3 triggers SQS message
  4. Image processor generates multiple resolutions (thumb, medium, full)
  5. Metadata stored in PostgreSQL
  6. Images served via CloudFront CDN

Feed Generation (Hybrid)

  • Normal users (<10K followers): Fanout-on-write
    • Post → push to all followers' Redis caches
  • Celebrities (>10K followers): Fanout-on-read
    • Feed request → fetch recent celebrity posts at read time

Feed Ranking

Score = 0.3×TimeDecay + 0.3×Engagement + 0.25×Affinity + 0.15×ContentType

  • Time Decay: half-life ~10 hours
  • Engagement: log(likes + 2×comments + 3×shares)
  • Affinity: past interaction frequency

Caching Layers

  1. Client: Last 50 feed items, profile (24h TTL)
  2. CDN: Images (24h default, 1yr max TTL, >95% hit rate)
  3. Redis: Feed cache (7d TTL, 2000 posts/user max)
  4. Database: Source of truth (Cassandra + PostgreSQL)

Stories

  • Redis with 24h TTL — auto-expires
  • Stored as sorted sets per user
  • Viewers list tracked for seen/unseen

Key Decisions

Decision Choice Why
Feed storage Cassandra High write throughput, partition by user_id
User data PostgreSQL ACID for account management
Image storage S3 11 nines durability, lifecycle policies
Image serving CloudFront CDN Low latency, >95% cache hit
Real-time cache Redis Cluster Sub-ms reads, TTL support
Async processing SQS + Workers Decouple upload from processing

Failure Modes

  • CDN down → fallback to S3 directly (slower but works)
  • Redis down → rebuild feeds from Cassandra (cold start)
  • S3 down → serve cached CDN copies (most images still accessible)
  • Upload processor down → images queue in S3, process later