Design a Scalable News Feed System
1. Problem Statement & Scope
System Mission
Design a massive-scale, low-latency social news feed platform (similar to Facebook News Feed, Twitter/X Timeline, or LinkedIn Feed) capable of publishing multimedia updates, dynamically aggregating friend and followee activities, ranking items by personalized relevance, and delivering continuous timeline scrolling for hundreds of millions of daily active users with sub-200ms latency.
Functional Requirements
- Feed Publishing (
POST /v1/posts): Users can publish status updates containing text, rich metadata, and media attachments (images/videos uploaded via pre-signed S3 URLs). - Feed Generation & Retrieval (
GET /v1/feed): Users view a real-time, aggregated timeline composed of posts from accounts they follow, sorted chronologically or ranked by a personalized machine learning recommendation score. - Hybrid Fanout Engine: Dynamically routes post distribution using fanout-on-write (push) for regular users and fanout-on-read (pull) for high-follower celebrity accounts.
- Follow / Unfollow Graph (
POST /v1/users/{id}/follow): Real-time management of bidirectional friendship edges and unidirectional follow graphs. - Infinite Cursor-Based Pagination: Seamless timeline scrolling using opaque cryptographic cursors preventing duplicate items or missed posts during concurrent insertions.
Non-Functional Requirements (SLAs & SLOs)
- High Availability: uptime SLA globally across multi-AZ AWS regions.
- Latency:
- Read Path (
GET /v1/feed): P95 , P99 . - Write Path (
POST /v1/posts): P95 , P99 . - Fanout Propagation Latency: of followers receive standard user posts in .
- Read Path (
- Consistency: Eventual consistency for timeline aggregation ( window); strict monotonic consistency and linearizability for post creation and author lookups.
- Durability: (11 Nines) zero-loss durability for post text, relational graph edges, and media assets.
2. Capacity & Scale Estimation
Traffic Calculations
- Daily Active Users (DAU): ().
- Daily Post Ingestion: new posts/day.
- Average Post Write QPS:
- Peak Post Write QPS ( peak factor):
- Feed Read Traffic: Each active user opens the app and refreshes their feed an average of times/day:
- Average Feed Read QPS:
- Peak Feed Read QPS ( peak factor):
- Read-to-Write Ratio: (Read heavy).
Storage Calculations (5-Year Horizon)
- Post Metadata Schema Size:
post_id: (64-bit Snowflake integer).author_id: .content: .media_urls: (JSON array of CDN pointers).created_at: (Unix epoch millisecond).metrics(likes, shares, comments): .- DynamoDB attribute overhead: .
- Total Metadata Size per Post: .
- 5-Year Post Metadata Storage: With DynamoDB Multi-AZ replication (): .
- Media Storage (Amazon S3):
- Assume of posts contain an image ( WebP) and contain a video ( MP4):
- Daily S3 Ingress: .
- 5-Year Media Storage: on Amazon S3 Standard with lifecycle transitioning to Glacier Flexible Retrieval after 90 days.
Network Bandwidth
- Ingress Bandwidth (Writes): (Almost all of this is media landing directly in S3 via pre-signed URLs; the Post Service API itself sees only .)
- Egress Bandwidth (Feed Reads):
- Each feed request returns 20 hydrated post objects ( payload):
- Media asset egress (images/videos) is served directly via Amazon CloudFront edge PoPs with an edge cache hit ratio, absorbing of raw media traffic.
Memory & Cache Sizing (ElastiCache Redis Timelines)
- Applying the 80/20 Pareto Principle: of registered users represent the active daily core ( active user timelines cached concurrently).
- Timeline Key Structure: Each active user maintains an ElastiCache Redis Sorted Set (
ZSET) containing the latest . - Memory per ZSET entry: (member string
post_id) + (score millisecond timestamp) + (SkipList node + Hash table entry) . Accounting for Jemalloc fragmentation and Redis overhead (): - Provisioned on an Amazon ElastiCache Redis Cluster with primary nodes ( per node usable), clustered mode enabled across 3 AZs, plus one replica per shard for failover (80 nodes total). A common sizing mistake is to quote the node count without multiplying it out: would be only , a third of the working set.
3. AWS-First High-Level Architecture
Synthesizing vector architecture diagram...
Follow a post from creation to a reader's feed. The publishing service saves the post in DynamoDB (media in S3) and emits an event to Kinesis. In the "Asynchronous Fanout & Event Ingestion" panel, fanout workers read the author's followers and push the post ID into each active follower's timeline sorted set in Redis; this is fan-out on write, so reading later is cheap. When someone opens the app, the "Feed Generation & Ranking Engine" panel runs four steps: read their precomputed timeline (1), fetch recent posts from celebrities they follow (2), score the candidates with a SageMaker model (3), and load the top 20 posts through DAX (4). Celebrities are excluded from fan-out because pushing one post to millions of timelines would be a huge write spike; the mix of push for normal users and pull for celebrities is the key idea.
Data Flow Walkthrough
- Post Creation Path:
- The client requests an S3 pre-signed upload URL from the Post Service, uploads images/videos directly to Amazon S3, and dispatches
POST /v1/posts. - Post Service validates the payload, generates a collision-free 64-bit Snowflake
post_id, writes the primary post record into Amazon DynamoDB, and publishes a lightweight post event to Amazon Kinesis Data Streams.
- The client requests an S3 pre-signed upload URL from the Post Service, uploads images/videos directly to Amazon S3, and dispatches
- Dynamic Fanout Execution:
- The Fanout Worker Fleet consumes from Kinesis. If the author has followers (standard user), the worker retrieves the follower list from DynamoDB and pipelines
ZADDcommands to write thepost_idinto each active follower's Redis timeline. - If the author has followers (celebrity), push fanout is skipped entirely. The post is simply marked in the author's own post partition.
- The Fanout Worker Fleet consumes from Kinesis. If the author has followers (standard user), the worker retrieves the follower list from DynamoDB and pipelines
- Feed Assembly & Read Path:
- Client invokes
GET /v1/feed. The News Feed Service fetches the calling user's precomputed timeline from ElastiCache Redis (Top 800 items). - Simultaneously, Feed Service retrieves the list of followed celebrities, queries their recent posts from DynamoDB, and executes a -way timestamp merge in memory.
- Merged candidates are passed to an Amazon SageMaker endpoint for ML scoring, the top 20 items are hydrated via DynamoDB Accelerator (DAX), and the response JSON is returned.
- Client invokes
Concrete Step-by-Step Request Walkthrough: Tracing Read & Write Cycles
| Step # | Event / Action | Component State | Distributed Transition | Output / Response |
|---|---|---|---|---|
| 1 | User usr_401 issues POST /v1/posts | Client holds JWT session token | ALB terminates TLS; routes to Post Service ECS task | HTTP 201 Created with post_id: "718293847561029384" returned in |
| 2 | Post Service commits write | DynamoDB Single-Table | PutItem writes USER#usr_401 / POST#<timestamp>#<post_id> | Post record safely replicated across 3 AWS Availability Zones |
| 3 | Event streaming to Kinesis | Non-blocking write to Kinesis shard | PutRecord executed with partition_key = "usr_401" | Event logged in append-only partition; offset acknowledged in |
| 4 | Fanout Worker evaluates threshold | Follower count query against DAX | Author follower count Standard User | Worker triggers Fanout-on-Write pipeline |
| 5 | Redis Timeline Pipeline write | Redis connection pool | Pipeline batches of 50 ZADD + ZREMRANGEBYRANK commands | 1,200 active follower timelines updated in |
| 6 | Follower usr_802 issues GET /v1/feed | Request routed to News Feed Service | Service queries ZREVRANGEBYSCORE timeline:usr_802 +inf -inf LIMIT 0 800 | Precomputed sorted candidate list retrieved in from Redis |
| 7 | Celebrity pull-merge phase | Parallel scatter-gather async IO | One parallel DynamoDB Query per followed celebrity (PK = USER#usr_celeb1, SK begins_with POST#, ScanIndexForward=false, Limit=20); BatchGetItem cannot express "latest N by author" because it needs exact keys | Merged candidate pool ( push + celebrity pull items) in memory |
| 8 | SageMaker ML precision ranking | SageMaker multi-model endpoint | Feature vector scored against user click/engagement model | Top 20 ranked post IDs selected |
| 9 | Batch post hydration | DAX cache check DynamoDB fallback | BatchGetItem retrieves full text, author metadata, and media URLs | JSON response formatted and returned to client () |
4. API Interface Design
1. Publish Post Endpoint
httpPOST /v1/posts Host: api.social.aws.internal Content-Type: application/json Authorization: Bearer <jwt_access_token> X-Idempotency-Key: 7b8e1f20-945a-497b-95eb-36b17c5b6102 { "content": "Designing high-scale distributed systems on AWS! #SystemDesign", "media_attachments": [ { "media_type": "IMAGE", "s3_url": "https://media.social.internal/uploads/2026/09/post_img_9912.webp", "width": 1920, "height": 1080 } ], "visibility": "PUBLIC" }
Response: 201 Created
json{ "status": "SUCCESS", "data": { "post_id": "718293847561029384", "author_id": "usr_401", "created_at": 1718000000120, "content": "Designing high-scale distributed systems on AWS! #SystemDesign", "media_attachments": [ { "media_type": "IMAGE", "url": "https://cdn.social.com/media/uploads/2026/09/post_img_9912.webp", "width": 1920, "height": 1080 } ] } }
2. Retrieve Feed Endpoint (Cursor Pagination)
httpGET /v1/feed?limit=20&cursor=eyJwb3N0X2lkIjoiNzE4MjkiLCJzY29yZSI6OTguMn0= Host: api.social.aws.internal Authorization: Bearer <jwt_access_token> Accept: application/json
Response: 200 OK
json{ "status": "SUCCESS", "data": { "items": [ { "post_id": "718293847561029384", "author": { "user_id": "usr_401", "username": "alex_architect", "avatar_url": "https://cdn.social.com/avatars/usr_401.webp", "is_verified": true }, "content": "Designing high-scale distributed systems on AWS! #SystemDesign", "media_attachments": [ { "media_type": "IMAGE", "url": "https://cdn.social.com/media/uploads/2026/09/post_img_9912.webp" } ], "created_at": 1718000000120, "metrics": { "likes": 1420, "reposts": 88, "comments": 64 } } ], "pagination": { "has_more": true, "next_cursor": "eyJwb3N0X2lkIjoiNzE4MjAxIiwic2NvcmUiOjg1LjR9" } } }
3. Follow / Unfollow User Endpoint
httpPOST /v1/users/usr_celeb99/follow Host: api.social.aws.internal Authorization: Bearer <jwt_access_token> Response: 200 OK { "status": "SUCCESS", "data": { "target_user_id": "usr_celeb99", "is_following": true, "target_is_celebrity": true, "followed_at": 1718000005000 } }
5. Data Models & Storage Architecture
DynamoDB Single-Table Design (SocialCoreTable)
The social graph, post metadata, and follower relationships are unified in a single high-performance DynamoDB table:
- Primary Partition Key (
PK): Entity identifier. - Primary Sort Key (
SK): Hierarchical sub-type and timestamp. - Global Secondary Index 1 (
GSI1): Followee index for reverse follower fanout queries. - Global Secondary Index 2 (
GSI2): Feed candidate retrieval by category or hashtag.
| Entity Type | Partition Key (PK) | Sort Key (SK) | GSI1-PK | GSI1-SK | Extended Attributes |
|---|---|---|---|---|---|
| User Profile | USER#<user_id> | METADATA | - | - | username, avatar_url, follower_count, is_celebrity |
| Published Post | USER#<user_id> | POST#<created_at>#<post_id> | POST#<post_id> | METADATA | content, media_json, created_at, likes_count |
| Follow Edge | USER#<follower_id> | FOLLOWS#<followee_id> | FOLLOWED_BY#<followee_id> | USER#<follower_id> | created_at, is_notifications_enabled |
| Celebrity Registry | GLOBAL#CELEBRITIES | USER#<user_id> | - | - | follower_count, last_post_timestamp |
ElastiCache Redis Timeline Cache Structure
- Data Structure: Redis Sorted Set (
ZSET). - Key:
timeline:usr_<user_id> - Member Value:
post_id(64-bit integer formatted as string). - Score: Millisecond timestamp (
created_at) for chronological feeds, or scaled float score () for ranked feeds. - Eviction & Trimming Policy: User timelines are strictly bounded to the most recent entries. Atomic trimming is enforced on every insertion:
text
ZADD timeline:usr_802 1718000000120 "718293847561029384" ZREMRANGEBYRANK timeline:usr_802 0 -801 EXPIRE timeline:usr_802 604800 # 7-day inactivity TTL
Cursor Pagination Over the Sorted Set
The cursor in GET /v1/feed is an HMAC-signed Base64 tuple (last_score, last_post_id), never an offset:
- Page 1:
ZREVRANGEBYSCORE timeline:usr_802 +inf -inf LIMIT 0 20. - Page N+1:
ZREVRANGEBYSCORE timeline:usr_802 (last_score -inf LIMIT 0 20(the(makes the bound exclusive), then drop any item withpost_id >= last_post_idat the boundary score to break ties. - Why not
OFFSET: while the reader scrolls, fanout keeps inserting newer posts at the top of the set. An offset-based page 2 would shift and re-show items from page 1; a score-based cursor is anchored to a specific post, so concurrent inserts can never duplicate or skip an item. - Signing: the HMAC stops clients from forging cursors into other users' score ranges, and a bad signature simply resets the client to page 1.
Unlock Complete Architecture & Production Runbooks
You have explored the free architectural preview (~43%). Spend 1 Coin to unlock the remaining 6 production deep-dive sections for a full 24 hours.