Clarify the requirementsCore idea
Functional
Post text, images and video. Follow and unfollow. See a personal feed of posts from people you follow. Like, comment and share.
Non-functional
500M daily active users and 1M posts a minute. The feed loads in under 200ms. Eventual consistency is fine: a feed can be a few seconds behind.
High-level architectureDesign
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
graph LR
Client["Client"] -->|"1. create post"| LB["Load balancer"]
Client -->|"5. load feed"| LB
LB --> PS["Post service"]
LB --> FS["Feed service"]
PS -->|"2. store"| PDB[("Post DB<br/>Cassandra")]
PS -->|"3. publish"| Q["Kafka"]
Q --> FO["Fan-out service"]
FO --> UG[("User graph<br/>Redis")]
FO -->|"4. push to followers"| FC["Feed cache<br/>Redis"]
FS -->|"6. read"| FC
class Client blue
class LB,PS,FS orange
class Q yellow
class FO green
class FC,UG red
class PDB violet
classDef orange fill:#ffd8a8,stroke:#1e1e1e,color:#1e1e1e
classDef blue fill:#a5d8ff,stroke:#1e1e1e,color:#1e1e1e
classDef green fill:#b2f2bb,stroke:#1e1e1e,color:#1e1e1e
classDef red fill:#ffc9c9,stroke:#1e1e1e,color:#1e1e1e
classDef violet fill:#d0bfff,stroke:#1e1e1e,color:#1e1e1e
classDef yellow fill:#ffec99,stroke:#1e1e1e,color:#1e1e1e
classDef grey fill:#e9ecef,stroke:#1e1e1e,color:#1e1e1ePost service
Creates, validates and stores posts. Stateless, so it scales out.
Feed service
Serves the personal feed from the Redis cache, 50 posts per request.
Fan-out service
The core. Reads post events from Kafka and adds the post ID to each follower’s feed cache, separately from posting.
Feed cache (Redis)
A sorted set per user, scored by time, holding the top 500 post IDs for instant reads.
User graph
Who follows whom: a Redis set per user, followers:{userId}.
Request flowsHow it works
Posting and fan-out
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
sequenceDiagram
participant A as User A
participant PS as Post service
participant DB as Cassandra
participant Q as Kafka
participant FO as Fan-out
participant Cache as Feed cache
A->>PS: create post
PS->>DB: store post
PS->>Q: publish post-created
PS-->>A: 201 created
Q->>FO: post event
FO->>FO: load A's followers
FO->>Cache: ZADD feed:B post_id time
FO->>Cache: ZADD feed:C post_id time
Note over FO,Cache: B and C now see the postLoading the feed
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
sequenceDiagram
participant B as User B
participant FS as Feed service
participant Cache as Feed cache
participant DB as Cassandra
B->>FS: GET /feed?cursor=abc
FS->>Cache: ZREVRANGE feed:B 0 49
alt cache hit
Cache-->>FS: top 50 post IDs
else cache miss
FS->>DB: build B's feed from the DB
DB-->>FS: posts
end
FS->>DB: batch-fetch post details
DB-->>FS: post contents
FS-->>B: feedDeep dive: fan-out strategiesPattern
This is the central design decision. There are two basic approaches, and the real answer combines them.
Fan-out on write (push)fast reads
When someone posts, push it into every follower’s feed cache right away. Reading a feed is then one cache lookup. The problem: a celebrity with 500M followers would need 500M cache writes for a single post.
Fan-out on read (pull)cheap writes
When someone opens their feed, fetch recent posts from everyone they follow and merge them. No write storms, but a feed load can mean hundreds of database queries.
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
graph TD
P["user creates a post"] --> D{"how many<br/>followers?"}
D -->|"~99% of users<br/>(under ~1000)"| Push["fan-out on write<br/>push to follower caches"]
D -->|"~1%: celebrities"| Pull["fan-out on read<br/>fetch at load time"]
Push --> M["merge when the feed loads"]
Pull --> M
class P blue
class D yellow
class Push green
class Pull orange
class M violet
classDef orange fill:#ffd8a8,stroke:#1e1e1e,color:#1e1e1e
classDef blue fill:#a5d8ff,stroke:#1e1e1e,color:#1e1e1e
classDef green fill:#b2f2bb,stroke:#1e1e1e,color:#1e1e1e
classDef red fill:#ffc9c9,stroke:#1e1e1e,color:#1e1e1e
classDef violet fill:#d0bfff,stroke:#1e1e1e,color:#1e1e1e
classDef yellow fill:#ffec99,stroke:#1e1e1e,color:#1e1e1e
classDef grey fill:#e9ecef,stroke:#1e1e1e,color:#1e1e1eDeep dive: feed rankingDesign
A purely chronological feed is simple but bad for engagement. Modern feeds rank posts using machine learning, based on signals like these:
Affinity
How often you interact with this person: likes, comments, profile visits.
Time decay
Newer posts rank higher: 2 hours old beats 2 days old.
Content type
Video ranks above images, images above links.
Engagement
Posts with many likes and comments get a boost.
score = affinity × 0.4 + engagement × 0.3 + time_decay × 0.2 + content_type × 0.1Deep dive: cachingScale
Each user's feed is a Redis sorted set: post IDs ordered by a score (here, the timestamp).
# Key: feed:{userId} Value: post IDs, score = timestamp
ZADD feed:user123 1725000000 post_abc
ZADD feed:user123 1725000060 post_def
# Latest 50 posts
ZREVRANGE feed:user123 0 49Keep the top 500 posts per user in Redis; past that, fetch from the database. That gives sub-100ms feed loads for about 99% of sessions.
| Cache sizing | Value |
|---|---|
| Memory | 500M users × 500 posts × 8 bytes ≈ 2 TB |
| Layout | Redis Cluster, sharded across nodes, so it fits in memory |
| TTL | 7 days: old posts age out of the cache |
Common interview mistakesPitfalls
Summary: key decisionsSummary
| Decision | Recommendation | Why |
|---|---|---|
| Fan-out | Hybrid: push, plus pull for celebrities | Handles celebrities without write storms |
| Feed cache | Redis sorted sets | Fast, ordered reads by time |
| Post storage | Cassandra | Built for appends, partitioned by user |
| Ranking | ML model (affinity + time + engagement) | Maximises engagement |
Frequently asked questionsFAQ
What is the difference between push and pull models for a news feed?
Push, or fan-out on write, pre-computes each follower's feed when a post is created, delivering fast reads but heavy write amplification for users with many followers. Pull builds the feed at read time by merging recent posts, giving cheap writes but slower, more expensive reads.
How do you rank posts instead of just showing them chronologically?
Compute a relevance score per post from recency, interaction volume, relationship strength, and content type, then serve the top results. Because ranking features shift constantly, score where it is cheap, write-time for hot users and read-time otherwise, and cache the computed feeds.
How do you handle celebrity accounts in a feed system?
Use hybrid fan-out. Proactively push posts out to ordinary followers, but hold celebrity posts for read-time merging or enqueue their fan-out asynchronously so writes stay fast. That avoids exploding write amplification on a single celebrity post while keeping the read path quick for the vast majority of users.

Now explain news feed to me out loud
I'll give you a design problem and push on the trade-offs, like a real interviewer.

