Design Instagram
Design Instagram step by step: photo upload, CDN delivery, follow-graph feeds, hybrid fan-out, stories, likes, and the reasoning behind every major decision.
Page content
Alice opens the camera, posts a photo of lunch, and closes the app. A few million people follow a celebrity who posts the same minute. Bob opens Home on a slow train and expects pictures, not a spinner.
Instagram is a photo-first feed, not a 280-character firehose. The follow-graph problem looks like Twitter. The new work is large binaries, a CDN, and a second, ephemeral product: Stories.
The main design question is:
How do we accept a photo without running it through the API process, show followers a fast Home, and avoid writing a celebrity post into tens of millions of inboxes?
We will start with metadata in a database and a photo that never enters the API process. Hybrid fan-out, CDN renditions, and Stories appear when that simpler path fails. This article teaches the feed from scratch. The Twitter walkthrough is the text-only cousin, not a prerequisite.
1. Clarify the problem
“Design Instagram” includes Reels ranking, shopping, DMs, and ads. That is too broad for one interview.
I would ask:
- Photo posts only, or video and Reels?
- Chronological Following feed or ranked Explore?
- Stories in scope?
- Likes and comments?
- Public follows only, or private accounts?
- How many DAU and how celebrity-heavy is the graph?
If the interviewer gives no extra constraints, I would state:
Product Post photo, follow, Home (following), profile, like, comment
Stories 24-hour photos; I would include them
Reels / Explore Mentioned; not the core path
DMs / shopping Out of scope
Feed Reverse-chronological Following first
Media Images (short video uses the same upload path)
Private accounts Fail closed on unauthorized read
Deployment One region for metadata; CDN is global
If they say “social feed” without a product, ask text or media. That single question picks Twitter’s emphasis or this one.
2. Functional requirements
The system must:
- Create a post with one or more images and a caption.
- Follow and unfollow.
- Read Home (posts from accounts the viewer follows).
- Read a user profile grid.
- Like and comment.
- Post a Story that disappears after 24 hours.
- Fetch an image from a CDN, not from the API.
The first version does not include:
- Reels ranking or a For You page;
- live video;
- shopping;
- full-text search of captions;
- direct messages.
3. Non-functional requirements
| Requirement | Target |
|---|---|
| Post ACK | p99 below 300 ms after metadata commit (upload is client → storage) |
| Home | p99 below 200 ms for a cached or precomputed page |
| Image bytes | CDN; API returns URLs |
| Celebrity post | Must not block ordinary posts |
| Story expiry | Best-effort within minutes of 24 h |
| Private account | Fail closed on unauthorized read |
Post row + follow edges source of truth
Home feed ids derived
Image bytes object storage + CDN
Like counts eventual
Stories TTL / expiry job
Edge cases to keep in mind
- The client retries the create-post call after the first commit succeeds.
- A celebrity posts to 40 million followers.
- Alice follows Bob and immediately opens Home.
- A leftover inbox id still points at a post after unfollow or a private-account revoke.
- Someone opens a Story after 24 hours.
- One celebrity thumbnail is a hot origin object.
- Transcode fails after the original upload succeeded.
- Two devices like the same post at once.
4. Estimate the scale
These are planning values, not Meta’s 10-K.
DAU 100 million
Posts / user / day 0.2 → 20 million posts/day
Home opens / user / day 8
Avg image after transcode 200 KB
Follows / user 200
Home reads:
100e6 × 8 / 86,400 ≈ 9,300 average QPS
20× peak ≈ 186,000 QPS
Post writes:
20e6 / 86,400 ≈ 230 posts/s average
Reads dominate. Fan-out on write for an average 200-follower post is 200 inbox writes — fine. A 40-million-follower post is not:
40,000,000 inbox writes
@ 100,000 writes/s → about 7 minutes for one post
We should not put that job on the request path, and we should not let it starve every ordinary post behind it.
Image egress on a peak Home refresh of 10 photos:
10 × 200 KB × 186,000 QPS ≈ 372 GB/s
That number is why thumbnails and a CDN exist. The API returns URLs. It does not io.Copy JPEGs.
Like writes can exceed posts by an order of magnitude. They still must not be UPDATE posts SET like_count = like_count + 1.
Bottlenecks, in order:
1. Home read QPS and hydrate
2. Celebrity fan-out write amplification
3. CDN / origin for hot thumbnails
4. Like uniqueness + count
5. Follow-graph reads on a naive pull Home
5. What are we storing?
User / profile
Follow edge follower → followee
Post caption, author, created_at, media keys
Media object keys, widths (feed, grid, story)
Like (post_id, user_id) unique
Comment partitioned by post_id
Story author, media, expires_at
Feed inbox user_id → [post_id] (ordinary authors)
Bytes live elsewhere:
origin://media/{media_id}/orig.jpg
origin://media/{media_id}/150.jpg
origin://media/{media_id}/320.jpg
origin://media/{media_id}/1080.jpg
Source of truth posts, follows, media keys, like pairs
Derived feed inboxes, like counts, search
Cache post objects, feed pages
Blobs object storage + CDN
Ephemeral story TTL sets, signed URL expiry
6. APIs
Keep JSON on the API. Keep bytes on object storage and the CDN.
Upload, then create the post
A 4 MB camera JPEG through POST /posts will timeout and cannot scale independently of metadata.
POST /v1/media/upload-url
Authorization: Bearer <alice>
Content-Type: application/json
{
"content_type": "image/jpeg"
}
HTTP 200 OK
{
"media_id": "md_1",
"upload_url": "https://objects.example/presigned/md_1"
}
The client PUTs the file to upload_url. Then:
POST /v1/posts
Authorization: Bearer <alice>
Idempotency-Key: 7c2e0b1a-4d55-4c3a-9f0e-2a1b6c8d9e01
Content-Type: application/json
{
"media_ids": ["md_1"],
"caption": "tuesday dal"
}
HTTP 201 Created
{
"post_id": "p_9f3a",
"created_at": "2026-08-22T04:21:00Z",
"status": "PROCESSING"
}
A retry with the same idempotency key returns the same post_id. It must not create a second post or a second fan-out.
Home, profile, social graph
GET /v1/feed/home?cursor=eyJjIjoiMjAyNi0wOC0yMiJ9&limit=10
Authorization: Bearer <bob>
HTTP 200 OK
{
"items": [
{
"post_id": "p_9f3a",
"author_id": "u_alice",
"caption": "tuesday dal",
"created_at": "2026-08-22T04:21:00Z",
"media": [
{
"media_id": "md_1",
"url": "https://cdn.example/md_1/320.jpg",
"width": 320,
"height": 400
}
],
"like_count": 1842,
"liked_by_me": false
}
],
"next_cursor": "eyJjIjoiMjAyNi0wOC0yMlQwNDoyMDo1OVoifQ"
}
GET /v1/users/{id}/posts?cursor=...
GET /v1/posts/{id}
POST /v1/posts/{id}/like
POST /v1/posts/{id}/comments
POST /v1/users/{id}/follow
DELETE /v1/users/{id}/follow
Home returns post ids plus hydrated objects. Image fields are CDN URLs, not base64.
If Bob is not allowed to see a private account’s post:
HTTP 404
{
"error": {
"code": "POST_NOT_FOUND",
"message": "Post not found"
}
}
Same 404 as a missing id. Do not leak that the post exists.
Stories
POST /v1/stories
GET /v1/stories/tray
GET /v1/users/{id}/stories
7. Basic data model
users
id, handle, is_private, created_at
follows
follower_id, followee_id, created_at
UNIQUE (follower_id, followee_id)
INDEX (followee_id, follower_id) fan-out: who follows Bob
INDEX (follower_id, followee_id) Home: who Alice follows
posts
id, author_id, caption, created_at, status
INDEX (author_id, created_at DESC) profile grid
post_media
post_id, media_id, position
media
id, object_key, width, height, status
likes
post_id, user_id
UNIQUE (post_id, user_id)
comments
id, post_id, user_id, body, created_at
INDEX (post_id, created_at, id)
stories
id, author_id, media_id, created_at, expires_at
INDEX (author_id, created_at DESC)
feed_inbox
user_id, created_at, post_id
PK (user_id, created_at, post_id)
outbox_events
id, type, payload, status PENDING|PUBLISHED
Celebrity authors are not written into 40 million inboxes. Their posts stay on the author timeline and are pulled at read time.
Do not store JPEG bytes or captions in feed_inbox. Captions change; hydration is cheaper than rewriting 200 copies.
8. Start with pull Home and a file on the API
A first version: PostgreSQL posts, POST /posts with a multipart file, Home merges the latest posts of everyone Alice follows.
Alice opens Home
│
▼
Load 200 followee ids
│
▼
Load recent posts from each author
│
▼
Merge by created_at
│
▼
Return 10 photos
That teaches the follow graph. It fails in three places:
- A 4 MB upload occupies an API pod.
- 186,000 Home QPS each merging 200 timelines will not stay under 200 ms.
- There is no answer for a 40-million-follower author except “the same merge, slower.”
We keep the post row and evolve upload and Home separately.
9. Photos do not enter the API pod
Alice
│
├─ 1. POST /media/upload-url → media_id + presigned PUT
│
├─ 2. PUT file ───────────────► object storage (orig.jpg)
│
└─ 3. POST /posts { media_ids }
│
▼
BEGIN
insert posts + post_media
insert outbox PostCreated
COMMIT
│
▼
201 + post_id (status PROCESSING)
A worker then builds renditions:
orig.jpg
│
▼
transcode
├─ 150.jpg profile grid
├─ 320.jpg Home / train
└─ 1080.jpg tap to open
│
▼
CDN
posts.status = READY
The post can stay PROCESSING until the feed-size rendition exists. Home should not show a broken image. Optional: show the post when 320px is ready, before 1080p finishes.
Never stream gallery bytes through the post service. Upload bandwidth and metadata QPS must scale independently — the same split as YouTube and invoices.
If transcode fails, keep the original, mark the post FAILED, and let Alice retry. Do not delete the source because a worker crashed.
10. The central decision: how should we generate Home?
There are two basic approaches:
Fan-out on read Build the feed when Alice opens Home
Fan-out on write Prepare Alice's feed when followed users publish
Neither is always better. We compare their cost against this workload: 186,000 peak Home QPS, ~230 posts/s, a few authors with tens of millions of followers.
11. Approach 1: fan-out on read
When Alice opens Home:
Alice
│
▼
Load accounts Alice follows
│
▼
Load recent posts from each account
│
▼
Merge by creation time
│
▼
Return the newest 10
If Alice follows 200 accounts, every refresh reads and merges 200 small grids.
Advantages
- Creating a post is cheap: one row.
- A celebrity post costs no more writes than Alice’s lunch photo.
- We do not store a separate home timeline for every user.
Disadvantages
- Read cost grows with following count.
- The same merge is repeated on every refresh.
- It is difficult to keep p99 below 200 ms at 186,000 peak QPS.
This is a good first product. It is also a useful fallback when precomputed feeds are down. At this Home QPS, it makes the dominant read path too expensive.
12. Approach 2: fan-out on write
Instead of waiting for Alice to read, prepare her inbox when Bob posts.
Bob creates post p_9f3a
│
▼
Find Bob's followers
│
▼
Add p_9f3a to each follower's inbox
│
▼
Alice's Home already contains p_9f3a
Alice’s feed is an ordered list of post ids:
inbox:alice
[p_9f3a, p_88, p_12, ...]
Reading Home becomes:
Alice
│
▼
Read top 10 post ids
│
▼
Fetch 10 post objects
│
▼
Attach CDN URLs
│
▼
Return
That is much cheaper than merging 200 author grids on every open.
Advantages
- Home reads are fast and predictable.
- We perform work once per follower instead of once per refresh.
- Cursor pagination is straightforward:
(created_at, post_id).
Disadvantages
- Every post creates many inbox writes.
- Inactive users receive updates they may never read.
- Authors with millions of followers create enormous fan-out jobs.
With an average of 200 followers:
20M posts/day × 200 followers
= 4 billion inbox writes/day
≈ 46,000 writes/s average
That can be distributed across workers. A celebrity is a different shape.
13. The celebrity problem
1 celebrity post
↓
40 million inbox writes
Even at 100,000 writes per second, that one fan-out takes about seven minutes.
We should not make the author wait, and we should not let one celebrity post delay every ordinary lunch photo behind it.
Pure fan-out on write solves the common case and fails the highest-degree users. Instagram does not get a special exemption because the payload is a photo. The inbox still stores ids.
14. The practical answer: hybrid fan-out
Use both models.
Ordinary author → fan-out on write
Celebrity author → fan-out on read
Alice has 800 followers
→ push p_9f3a into each follower inbox
Celeb has 40M followers
→ store the post only on Celeb's profile grid
When Bob opens Home:
Bob's precomputed inbox
+
Recent posts from celebrities Bob follows
│
▼
Merge by time
│
▼
Hydrate posts
│
▼
Re-check follow + private visibility *now*
│
▼
Sign CDN URLs
│
▼
Return top 10
Most people follow only a few very large accounts, so this merge is far cheaper than merging every followed account.
Start with a measured threshold, not a brand name:
followers < 10,000 push
followers ≥ 10,000 pull
Tune it using active follower count, fan-out completion time, worker capacity, and storage cost. The threshold is an operational knob.
The hybrid model is more complex, but it handles both the lunch photo and the celebrity drop.
15. High-level architecture
Now we have enough to draw the main boxes.
┌──────────────┐
│ Clients │
└──────┬───────┘
│
PUT image │ JSON
│ │
▼ ▼
Object storage API Gateway
│ Auth + limits
│ ┌────┴────┐
│ ▼ ▼
│ Post / Follow Feed / Stories
│ │ │
│ ▼ ▼
│ posts + outbox inbox + tray
│ │
│ ▼
│ Kafka
│ │
│ ├─ fan-out workers
│ ├─ transcode workers
│ └─ search / counts
│
▼
CDN ◄── renditions
Write path in words:
- Client uploads bytes to object storage with a presigned URL.
- Post service writes the post, media keys, and an outbox row in one transaction.
- The API returns without waiting for fan-out or 1080p.
- Workers transcode, fan-out ordinary authors, and update derived indexes.
Read path in words:
- Feed service reads the inbox page.
- It adds recent posts from celebrities Bob follows.
- It hydrates post objects, filters visibility, and returns CDN URLs.
16. Why the outbox and Kafka
BEGIN
insert posts
insert outbox PostCreated
COMMIT
│
▼
publisher → Kafka → fan-out / transcode / search
If we COMMIT then kafka.Publish and the process dies, the post exists and no follower ever sees it in their inbox. If we publish first and the insert rolls back, workers fan-out a post that does not exist.
The outbox makes “the post exists” and “the event will be published” succeed or fail together. Kafka being down only delays workers. See Transactional Outbox.
Fan-out workers must be idempotent. Duplicate Kafka delivery must not insert p_9f3a twice into Alice’s inbox. The inbox primary key (user_id, created_at, post_id) does that.
Do not publish from inside the request transaction. That holds the row open while the network talks.
17. Fan-out workers
PostCreated p_9f3a author=Alice
│
▼
Is Alice over the celebrity threshold?
│
yes │ no
│ │
│ ▼
│ page Alice's followers
│ │
│ ▼
│ insert inbox (follower, created_at, p_9f3a)
│
▼
done (pull on read)
Page follower lists. Do not load 40 million ids into one worker even for a “push” author who just crossed the threshold — that author should already be pull.
If fan-out lags, Alice still sees her own post on her profile (author timeline). Bob can miss it for a few seconds. That is acceptable. A missing post after commit is not.
Unfollow race: fan-out may still write p_9f3a into Bob’s inbox after Bob unfollowed Alice. Hydrate must re-check the follow edge. The inbox is a hint.
18. Reading the feed
GET /feed/home?cursor=...
│
▼
Read inbox page for Bob (ordinary authors)
│
▼
Load celebrity ids Bob follows
│
▼
Read those authors' recent READY posts
│
▼
Merge by (created_at, post_id)
│
▼
Batch-get post + media
│
▼
Drop posts Bob must not see
│
▼
Attach like_count, liked_by_me, CDN URLs
Store ids in the inbox, not JPEG bytes. Hydration is a batch get of ~10 posts. Cache post:{id} in Redis with a short TTL.
Pagination is a cursor on (created_at, post_id), never OFFSET. Celebrity merge must use the same cursor so a pulled post does not jump above an already-seen inbox item on the next page.
If Redis is down, Home degrades to the durable inbox and the post table. Slower, still correct.
19. Follow, unfollow, and “I followed and opened Home”
Alice follows Bob
insert follows
optional: backfill Bob's last N posts into Alice's inbox
A full backfill of years of photos is wasteful. Backfill a small window (for example 20 recent posts) or skip backfill and pull Bob’s latest posts on the next Home read if fan-out has not run yet.
Unfollow:
delete follows
do not scan the whole inbox to delete Bob's ids
Leftover ids are filtered at hydrate. A sweeper can trim later. Immediate inbox surgery on unfollow is optional, not required for correctness.
Private account: when Bob sets is_private or revokes Alice, the same hydrate check fails closed. Cache is not authorization.
20. Profile grid
GET posts
WHERE author_id = Bob
AND status = READY
ORDER BY created_at DESC
Partition by author_id. This is fan-out on read of one timeline. It is cheap. Thumbnails are 150px CDN objects.
The author viewing their own profile can include PROCESSING posts. Followers should not.
21. Likes and comments
Likes:
INSERT likes (post_id, user_id) -- unique pair
INCR like:{post_id} -- Redis, eventual
A retry is a no-op on the unique pair. The count can lag. Do not UPDATE posts. A celebrity post’s likes are a hot partition on post_id; they still must not live as a counter on the post row. Same idea as YouTube views.
Comments: cursor on (post_id, created_at, id). Do not OFFSET. Hide or tombstone instead of a hard delete if you need audit.
22. Stories are a different clock
A Story is not a post with expires_at shoved into the Home inbox.
stories
author_id + created_at
expires_at = created_at + 24h
Tray (the circles on top of Home):
users Alice follows who have a non-expired story
Alice
│
▼
Load followee ids
│
▼
Intersect with story:active:{author} (Redis SET / key with TTL 24h)
│
▼
Return tray order (recent activity first)
When the TTL dies, the circle disappears. A sweeper deletes blobs after expiry. GET /users/{id}/stories after 24 hours returns empty, not the photo.
Do not fan-out Stories into 40 million inboxes. A celebrity Story is one object. The tray pulls recent active authors Alice follows. If that set is large, keep story:active keys and probe the followees Alice actually follows — do not scan all active stories on earth.
Views (“seen”) are a watermark or a set with TTL, not a forever fact table, unless the product needs analytics.
Do not merge Stories into feed_inbox. Different ranking, different expiry, different UI.
23. Caching and CDN
post:{id} metadata
feed:{user}:page short TTL
story:active:{author} TTL 24h
image / thumb CDN in front of object storage
A celebrity photo is a hot origin key. Several widths plus a CDN keep origin alive. Redis is not the post store and not the JPEG store.
Signed CDN URLs expire. Rotation does not require rewriting inboxes.
24. Consistency
Strong post insert, follow edge, like unique row
Eventual follower Home, like counts, story tray
Immediate-ish author sees own post on profile
Fail closed private / unfollow at hydrate
Alice posts and opens her profile: read her author timeline, not the fan-out. Follow-then-Home: mix a short pull of Bob’s latest posts if the inbox is still catching up.
25. Failure scenarios
| Failure | Behavior |
|---|---|
| Object storage down | New upload fails; warm CDN hits still render |
| Catalog / post DB down | No new posts; cached Home may still show old ids until hydrate fails |
| Kafka down | Outbox waits; author profile still has the post |
| Redis down | Home slower; durable inbox + post table still work |
| Transcode fail | Post stays PROCESSING/FAILED; source kept |
| Celebrity post | No million writes; read path does extra merge |
| Fan-out lag | Profile is correct; follower Home is briefly stale |
| Story after 24h | Tray omits; GET returns empty |
| Unfollow leftover id | Hydrate drops the post |
26. Observability, security, scale
Log request_id, user_id, post_id, media_id. Do not log presigned query strings as if they were public.
Metrics that matter:
post_ack_latency
home_p99
fanout_lag_seconds
fanout_writes_per_post
transcode_queue_age
cdn_origin_bytes
like_conflict_rate
story_tray_latency
Security: every Home hydrate re-checks follow and privacy. Rate-limit create-post, follow, and like (rate limiter). Same 404 for missing and forbidden posts. Idempotency keys on create.
Scale:
1M DAU pull Home + object storage may still work
10M DAU push inboxes for ordinary authors, CDN
100M DAU hybrid threshold, inbox store, transcode fleet
Celebrity never push; isolate their comment/like partitions
Partition posts by author_id or post_id. Inboxes by user_id. Celebrity follower lists by author_id + bucket if you still need to notify, not to write inboxes. Media by object key on the blob store.
Multi-region: Home is read-heavy and cacheable. Writes have a home region per user. CDN is already global.
Explore / Reels is a recommendation problem: candidates, then rank — see YouTube Home. Do not pretend it is a bigger inbox.
27. Where logic lives
internal/media upload URL, status, rendition keys
internal/post create, idempotency, outbox
internal/follow edges, privacy
internal/feed inbox page, celebrity merge, hydrate
internal/story create, tray, expiry
internal/social likes, comments
internal/fanout worker, threshold, idempotent insert
Handlers parse HTTP. Fan-out does not run inside BEGIN. Tests: two creates with one idempotency key; leftover inbox id after unfollow must not appear; two threads like once.
28. How this differs from Twitter
| Payload | Short text | Images (and short video) |
| Upload | Tiny | Presigned PUT + transcode |
| Home | Hybrid fan-out of ids | Same hybrid of ids |
| Celebrity | Pull tweets | Pull posts; same write-amp math |
| Extra clock | Little | Stories, 24 h TTL |
| Profile | Tweet timeline | Photo grid + small thumbs |
| Bytes | Optional media | Always a CDN problem |
The feed pattern is shared. The interview time after you name hybrid fan-out belongs on media and Stories. Naming the shared pattern and then spending the hour on JPEGs is Senior judgment, not laziness.
29. Final architecture
WRITE
─────
Client
→ POST /media/upload-url
→ PUT orig.jpg → object storage
→ POST /posts { media_ids }
→ Post store + Outbox
→ Return 201 (PROCESSING)
→ Kafka
├─ transcode → 150 / 320 / 1080 → CDN
└─ fan-out ordinary authors → inbox store
READ
────
Client
→ GET /feed/home
→ Inbox page
+ Celebrity author grids
→ Merge by time
→ Hydrate posts
→ Re-check follow + privacy
→ Return CDN URLs
STORIES
───────
POST /stories → stories row + story:active TTL 24h
GET /stories/tray → followees ∩ active keys
After 24h → tray omits, blob sweeper deletes
Photo is a blob
→ storage + CDN
Home is read-heavy
→ precompute ordinary inboxes
Celebrity write-amp
→ pull on read
Stories are ephemeral
→ TTL tray, not the inbox
30. Interview-ready summary
How to walk through in 10–15 minutes
0–2 min. Photo-first feed. Upload vs Home vs celebrity.
2–5 min. Presigned upload, APIs, idempotency, data model.
5–9 min. Pull vs push vs hybrid fan-out, outbox, hydrate + privacy.
9–12 min. Stories TTL, likes, CDN renditions.
12–15 min. Failures, unfollow leftover, Twitter contrast.
Key decisions to remember
- Scope to post, follow, Home, profile, like/comment, Stories — not Reels ads.
- Presigned upload; API stores media keys.
- Start from pull Home; show why 186k QPS cannot merge 200 timelines.
- Hybrid fan-out: push ordinary authors, pull celebrities.
- Inboxes hold post ids; hydrate and re-check privacy.
- Outbox so a committed post is not lost to Kafka; fan-out is idempotent.
- Stories use a TTL tray, not the main feed inbox.
- Likes are unique pairs; counts are eventual.
- CDN for bytes; never stream images through the post service.
- Author profile is fan-out on read of one timeline.
Likely interviewer follow-up questions
- How is this different from Twitter?
- How do you upload a 4 MB photo?
- Why not put images in the feed table?
- Why hybrid instead of “more Kafka”?
- How do Stories expire?
- What happens when you follow someone and open Home immediately?
- How do you hide a post after unfollow?
- How do you resize images?
- Would you fan-out Stories to 40 million followers?
- What if Redis is down during Home?
- How do you paginate after merging celebrity posts?
Senior-level points that differentiate the answer
- Teach pull, then push, then celebrity, then hybrid — do not jump to Kafka.
- Separate the blob path from the metadata path.
- Pull celebrities; do not apologize with “we will add more workers.”
- Filter private visibility at read time; the inbox is a hint.
- Treat Explore as ranking, not as a bigger inbox.
- Quote image egress once, then refuse to put bytes on the API.
- Idempotent create and idempotent fan-out are different problems.
A 1–2 minute verbal answer
I would scope Instagram to photo posts, follow, a chronological Following Home, profile grid, likes, comments, and 24-hour Stories. At 100 million DAU, Home is the QPS problem and celebrity fan-out is the write-amplification problem. Images never go through the API: the client uploads to object storage, we transcode a few widths, and the API returns CDN URLs.
I would start Home as fan-out on read, then precompute inboxes because 186,000 peak Home opens cannot merge 200 author grids. Pure push fails for a 40-million-follower account, so the feed is hybrid: persist the post and an outbox row, fan-out the id to ordinary followers, skip celebrities, and merge their latest posts at read time. Hydration re-checks follow and private-account rules. Stories are a separate TTL’d tray, not inbox rows. Likes use a unique
(post_id, user_id). If Kafka is down, the outbox waits and the author’s profile still shows the photo.
For the text-only feed, see Design Twitter. For the broader interview framework, see the System Design Interview Complete Guide.
