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
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 & reliabilityHow 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 decompositionAnalyze 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 degradationQuiz
1. Why does Instagram use fanout-on-write for normal users but fanout-on-read for celebrities?
2. What is the primary purpose of generating multiple image resolutions during upload?
3. Why is Cassandra preferred over PostgreSQL for the feed data table?
4. How does the system ensure images are never lost (11 nines durability)?
5. What is the benefit of using a signed URL for direct client-to-S3 uploads?
6. How do Stories handle expiration without expensive database scans?
Flashcards
Question
What is fanout-on-write vs fanout-on-read?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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
- Client requests pre-signed S3 URL
- Client uploads directly to S3
- S3 triggers SQS message
- Image processor generates multiple resolutions (thumb, medium, full)
- Metadata stored in PostgreSQL
- 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
- Client: Last 50 feed items, profile (24h TTL)
- CDN: Images (24h default, 1yr max TTL, >95% hit rate)
- Redis: Feed cache (7d TTL, 2000 posts/user max)
- 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