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:
- Read-heavy workloads with massive scale (500M+ DAU)
- Media storage and delivery optimization
- Social graph operations (followers, feed generation)
- Hybrid fanout strategies for feed distribution
- Real-time features with 24-hour TTL content
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
- Daily Active Users: 500M
- Monthly Active Users: 1B
- Photos uploaded per day: 100M
- Average photo size: 2MB (original)
- Follows per user: 200 (average), 50M (max for celebrities)
- Feed views per user per day: 30
- Stories per user per day: 5 (active users)
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)
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 |
3. High-Level Design
3.1 System Architecture
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
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
- Chunked Upload: Split large files into 5MB chunks, parallel upload, resume support
- Client-Side Resize: Mobile app resizes to max 1080p before upload (saves bandwidth)
- EXIF Stripping: Remove geolocation, camera metadata (privacy, 10-20% size reduction)
- Format Negotiation: Serve WebP to Chrome, AVIF to Safari, JPEG fallback
- Lazy Processing: Generate large/original on-demand, always process medium/thumb
- CDN Pre-warming: Push processed images to CDN for users with >100K followers
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
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)
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
~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
}
7. Stories Feature
7.1 Stories Characteristics
- 24-hour TTL: Automatic expiration, no manual deletion needed
- Ephemeral storage: Lower durability requirements (99.9% vs 99.9999%)
- High view rate: 60% of followers view stories (vs 5% for posts)
- Sequential viewing: Users watch multiple stories in succession
- Real-time delivery: Ring indicator shows new stories
7.2 Stories Architecture
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
8. Explore Page
8.1 Explore Objectives
- Discovery: Show content from users you don't follow
- Personalization: Based on your interests and engagement patterns
- Diversity: Mix of trending, interest-based, location-based content
- Freshness: Balance between popular and recent content
8.2 Explore Architecture
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)
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
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:
- Image processing: Client gets 202 Accepted immediately, processing happens async
- Feed fanout: Post write returns success, fanout to followers happens in background
- Notification delivery: Push notifications sent async (eventual delivery)
- Analytics events: Like/view/impression events batched and streamed to data warehouse
- Search indexing: Elasticsearch index updates via change data capture (CDC)
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)
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)
- Feed load P99 < 300ms: Measured from API request to full post metadata return (excludes image download)
- Image upload success > 99.5%: Ratio of successful uploads to total upload attempts
- Explore page refresh < 500ms: Time to generate and return personalized explore grid
Monitoring & Alerting
- Feed cache hit rate: Monitor Redis cache hit rate (target > 85%), alert if drops below 70% (indicates cache invalidation issues or hot celebrity posts)
- Image processing pipeline latency: Track time from upload to all resized versions available (alert if P95 > 10s)
- Upload queue depth: Monitor Kafka queue depth for image processing (alert if lag > 60s, scale workers)
Deployment Strategy
- Canary feed ranking changes to 5% users: Deploy new ML ranking model to 5% of traffic, monitor engagement metrics (like rate, session duration) for 24 hours before full rollout
- A/B test explore algorithm: Split traffic 50/50 between current and new explore candidate generation, measure discovery rate and user retention
Circuit Breakers
- Feed service → recommendation ML: If ML service latency > 2s or error rate > 5%, fall back to chronological feed ranking (degrade gracefully)
- Upload → image processing: If processing queue depth > 100K, throttle new uploads to 50% capacity (prevent cascade failure)
Disaster Recovery
- User data cross-region: PostgreSQL multi-region replication with RPO: 1 minute (async replication lag), RTO: 5 minutes (DNS failover)
- Feed cache is ephemeral: Redis feed cache can be lost (no persistence), rebuilt from post store via fanout workers within 30 minutes