Social Media Feed API
This example follows the system design process from lesson 01 to design a social media feed with REST, an API gateway, and cursor pagination.
Step 1: Requirements
Functional:
- Users create posts
- Users see a feed of posts from people they follow
- Users can like and delete posts
- Feed is paginated (infinite scroll)
Non-functional:
- Feed loads in under 200ms
- 1 million daily active users
- Feed can be slightly stale (eventual consistency is fine)
- Must handle bots scraping feeds (rate limiting)
What we're NOT building: the follow system, notifications, or search. Just the feed and posts.
Step 2: Estimation
DAU: 1 million
Feed loads per user per day: 20 (scrolling multiple times)
Total feed reads: 20M/day = ~230/sec
Peak (3x): ~700 reads/sec
New posts per day: 500,000
Posts per second: ~6/sec
Read-to-write ratio: ~40:1 (heavily read)
Storage per post: 500 bytes (text, author, timestamp, metadata)
Daily post storage: 500K × 500B = 250 MB
Yearly: ~90 GBRead-heavy system. Caching the feed will eliminate most database load. A single database with a cache in front handles this.
Step 3: High-Level Design
Client
↓
API Gateway
(auth, rate limit)
↓
┌────┴────┐
▼ ▼
Feed Post
Service Service
│ │
▼ ▼
Cache Database
│
▼
DatabaseAPI Gateway handles auth (validate JWT), rate limiting (300 reads/min per user, 30 posts/min), and routing (/feed → Feed Service, /posts → Post Service).
Feed Service reads from cache first. On cache miss, queries the database for posts from followed users, caches the result.
Post Service handles creates and deletes. On new post, publishes an event to invalidate/update cached feeds of followers.
Step 4: Deep Dive
API Design (REST)
POST /v1/posts → Create a new post
GET /v1/feed → Get your feed (paginated)
GET /v1/posts/:id → Get a single post
DELETE /v1/posts/:id → Delete your own post
POST /v1/posts/:id/like → Like a postCursor Pagination
A feed changes constantly. New posts appear at the top. Offset pagination would break:
- You load posts 1-10 (page 1)
- Someone you follow posts something new
- You request page 2 (offset 10) — everything shifted down
- Post 10 appears again on page 2. Duplicate.
Cursor pagination fixes this:
GET /v1/feed?limit=10
→ Returns posts + cursor
GET /v1/feed?limit=10&after=eyJ0IjoiMjAyNi0wNS0xOVQwOTowMCJ9
→ Returns next 10 posts after that timestampThe cursor encodes the last post's timestamp. The query: WHERE created_at < :cursor AND author_id IN (followed_users) ORDER BY created_at DESC LIMIT 10. New posts at the top don't shift your position.
API Gateway in Action
- Request arrives:
GET /v1/feed - Gateway validates JWT → extracts
user_id: u_456 - Gateway checks rate limit:
INCR rate:u_456:feedin Redis. Under 60/min? Continue. - Gateway routes to Feed Service with user_id attached
- Feed Service checks cache → hit? Return immediately. Miss? Query DB, cache result, return.
Versioning
/v1/feed returns {id, text, author, timestamp}. Later, /v2/feed adds {reactions_count, is_liked}. Both run simultaneously. Old mobile app versions still use v1. The gateway routes based on the URL prefix.
Response
{
"posts": [
{
"id": "p_789",
"author": "alice",
"text": "Just deployed to production",
"created_at": "2026-05-19T09:00:00Z",
"reactions_count": 12
}
],
"pagination": {
"next_cursor": "eyJ0IjoiMjAyNi0wNS0xOVQwODo1MCJ9",
"has_more": true
}
}The client doesn't decode the cursor. It just passes it back on the next request.
Concepts Used
| Concept | Lesson | How it's used here |
|---|---|---|
| System design process | 01 | Requirements → Estimation → Design → Deep dive |
| Estimation | 02 | QPS, storage, read/write ratio |
| Caching | 07 | Feed responses cached, eliminates most DB reads |
| Database | 09 | Stores posts, follows, likes |
| Message queues | 15 | New post event invalidates follower feed caches |
| API gateway | 17 | Auth, rate limiting, routing, versioning |
| REST + pagination | 17 | Resource endpoints, cursor-based feed scrolling |
| Rate limiting | 19 | Per-user limits on reads and writes |