Design Instagram
Instagram is two systems in one: a media pipeline that stores and delivers photos/videos at massive scale, and a feed system that assembles each user’s personalized home timeline. The defining decision is how to build the feed — fan-out-on-write (precompute timelines) vs. fan-out-on-read (assemble on request) — and a hybrid that handles celebrities with millions of followers.
Design it yourself
Don't just read it — drag components onto a canvas and get Aria's interviewer review.
Requirements
Functional
- Upload photos and videos with captions
- Follow / unfollow other users
- Home feed — recent posts from people you follow, newest first
- Like and comment on posts
- View a user’s profile grid and explore/discover content
Non-Functional
- 500M+ users, extremely read-heavy (feed loads ≫ posts)
- Home feed latency < 200ms at p99
- Media stored durably and delivered fast worldwide
- High availability — a stale feed is fine, a down feed is not
- Handle celebrities with 100M+ followers without meltdown
Capacity Estimation
| Users | 500M total, ~300M daily active |
| Posts / day | ~100M → ~1,150 writes/sec |
| Feed reads | billions/day → ~100k+ reads/sec |
| Avg media size | ~1.5 MB (post-compression) |
| Read:write ratio | ~100:1 — cache everything hot |
High-Level Components
Client + CDN
The app loads feed metadata from the API and pulls the actual images/videos from a CDN edge close to the user. The CDN serves the vast majority of media bytes, keeping origin load and latency low.
API Gateway / Load Balancer
TLS, auth, rate limiting, and routing to the Media, Feed, and Graph services. Stateless and horizontally scaled.
Media Service + Object Store (S3)
Handles uploads, stores originals in an object store (S3), and kicks off an async transcoding/thumbnail pipeline. Generated renditions are pushed to the CDN. Media bytes never live in the database.
Post & Graph Store (NoSQL — Cassandra)
Posts, the follower/following graph, likes, and comments live in a wide-column NoSQL store (Cassandra) partitioned for high write throughput and horizontal scale. The access pattern is key-based and append-heavy.
Feed Service (fan-out)
Builds home timelines. On a new post it fans out the post id into followers’ precomputed timeline lists (fan-out-on-write); for celebrities it switches to fan-out-on-read to avoid writing to 100M lists.
Timeline Cache (Redis)
Stores each active user’s materialized timeline (a list of post ids) in Redis so a feed load is a single fast cache read, not a scatter-gather across the graph.
Fan-out Queue (Kafka)
A new post is published to Kafka; workers asynchronously fan it out to follower timelines. This keeps the upload request fast and absorbs bursts.
Architecture Diagram
Deep Dives
The Feed: Fan-out-on-write vs. Fan-out-on-read
This is the interview’s crux.
Fan-out-on-write (push): when a user posts, immediately append the post id to every follower’s cached timeline. Feed reads are then trivially fast — just read your precomputed list. Cost: a post by someone with 1M followers triggers 1M writes.
Fan-out-on-read (pull): store nothing per-follower; when a user opens the app, gather recent posts from everyone they follow and merge. Cheap writes, but expensive, high-latency reads (a scatter-gather across potentially thousands of followees).
Reality: hybrid. Push for normal users (fast reads dominate a 100:1 workload). For celebrities (millions of followers), skip the fan-out and pull their recent posts at read time, merging them into the pushed timeline. This avoids the "write to 100M lists" explosion while keeping the common case fast.
Hybrid feed — pushed timeline + pulled celebrity posts
// Read path — hybrid feed assembly
List<Long> timeline = redis.lrange("feed:" + userId, 0, 200); // pushed posts
List<Long> celebPosts = following.stream()
.filter(u -> u.isCeleb()) // pulled at read time
.flatMap(u -> recentPosts(u.id()).stream())
.toList();
return merge(timeline, celebPosts) // merge + sort by time
.stream().sorted(byTimeDesc()).limit(50).toList();Media Pipeline — Store Once, Deliver Everywhere
Uploads go to the Media Service, which writes the original to an object store (S3) and enqueues a transcoding job. Workers generate multiple renditions — thumbnail, feed-size, full — and multiple formats, then push them to the CDN.
Why a CDN? Media is the bulk of the bytes and the same popular photo may be viewed millions of times. Serving it from edge nodes close to users gives low latency and offloads the origin. The API only ever returns *URLs* to CDN objects, never the bytes.
This is the same metadata-vs-blobs split as a file store: small structured records in the DB, big immutable blobs in object storage fronted by a CDN.
Media upload — object store + async transcode + CDN URL
// Upload → store original → async transcode → CDN
String key = objectStore.put(userId, originalBytes); // S3
kafka.publish("media.transcode", new TranscodeJob(key)); // async renditions
post.mediaUrl = cdn.url(key); // API returns a CDN URL
postStore.save(post); // metadata onlyWhy NoSQL for Posts and the Social Graph
The post and graph workload is append-heavy, key-addressed, and enormous — "give me user X’s recent posts", "give me X’s followers". There are no complex joins or multi-row transactions on the hot path.
A wide-column store like Cassandra fits perfectly: partition by user id, cluster by time, and you get fast writes and fast "recent posts for a user" range scans, with horizontal scale and no single-master bottleneck. Likes and comments follow the same pattern (partition by post id).
Counts (likes, followers) that would be hot single rows are handled with distributed counters or cached aggregates rather than a `SELECT COUNT(*)` on every view.
Caching the Timeline
At 100k+ feed reads/sec, every read must be cheap. Each active user’s materialized timeline (a list of ~hundreds of recent post ids) lives in Redis, so opening the app is a single `LRANGE` rather than a graph traversal.
The post objects themselves (author, caption, media URL, counts) are also cached, so hydrating a timeline is a batched multi-get from cache. Only cold users or cache misses fall through to Cassandra. This is what turns a fundamentally expensive "assemble a personalized feed" operation into a sub-200ms response.
Key Trade-offs
Feed generation strategy
Pure push explodes on million-follower accounts; pure pull makes every read a slow scatter-gather. The hybrid keeps the 100:1 read case fast while taming celebrity fan-out.
Where does media live?
Media is the bulk of the bytes and is read enormously. An object store gives durable cheap storage; a CDN gives low-latency global delivery and offloads the origin.
Posts & graph store: SQL vs. NoSQL
The workload is append-heavy, key-addressed, and huge, with no hot-path joins. Cassandra scales writes horizontally; a single relational master would bottleneck.
Feed consistency
Seeing a post a second or two late is fine; availability and latency matter far more. This freedom is exactly what lets the feed be aggressively cached and fanned out async.
Interview Tips
- 1Frame it as two systems: a media pipeline and a feed system — then go deep on the feed.
- 2The fan-out-on-write vs. -on-read trade-off is the whole interview; land the hybrid and the celebrity problem.
- 3Media → object store + CDN, DB holds only metadata/URLs. Same split as a file-storage system.
- 4Justify NoSQL (Cassandra) by the access pattern: append-heavy, key-based, no hot-path joins.
- 5Explain the timeline cache (Redis list of post ids) as what makes sub-200ms feeds possible.
- 6Handle counts (likes/followers) with distributed counters or cached aggregates, not COUNT(*) per view.
Discussion
Discussion
Sign in to join the discussion.