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:

  1. Create a post with one or more images and a caption.
  2. Follow and unfollow.
  3. Read Home (posts from accounts the viewer follows).
  4. Read a user profile grid.
  5. Like and comment.
  6. Post a Story that disappears after 24 hours.
  7. 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

RequirementTarget
Post ACKp99 below 300 ms after metadata commit (upload is client → storage)
Homep99 below 200 ms for a cached or precomputed page
Image bytesCDN; API returns URLs
Celebrity postMust not block ordinary posts
Story expiryBest-effort within minutes of 24 h
Private accountFail 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:

  1. A 4 MB upload occupies an API pod.
  2. 186,000 Home QPS each merging 200 timelines will not stay under 200 ms.
  3. 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:

  1. Client uploads bytes to object storage with a presigned URL.
  2. Post service writes the post, media keys, and an outbox row in one transaction.
  3. The API returns without waiting for fan-out or 1080p.
  4. Workers transcode, fan-out ordinary authors, and update derived indexes.

Read path in words:

  1. Feed service reads the inbox page.
  2. It adds recent posts from celebrities Bob follows.
  3. 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

FailureBehavior
Object storage downNew upload fails; warm CDN hits still render
Catalog / post DB downNo new posts; cached Home may still show old ids until hydrate fails
Kafka downOutbox waits; author profile still has the post
Redis downHome slower; durable inbox + post table still work
Transcode failPost stays PROCESSING/FAILED; source kept
Celebrity postNo million writes; read path does extra merge
Fan-out lagProfile is correct; follower Home is briefly stale
Story after 24hTray omits; GET returns empty
Unfollow leftover idHydrate 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

TwitterInstagram
PayloadShort textImages (and short video)
UploadTinyPresigned PUT + transcode
HomeHybrid fan-out of idsSame hybrid of ids
CelebrityPull tweetsPull posts; same write-amp math
Extra clockLittleStories, 24 h TTL
ProfileTweet timelinePhoto grid + small thumbs
BytesOptional mediaAlways 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

  1. Scope to post, follow, Home, profile, like/comment, Stories — not Reels ads.
  2. Presigned upload; API stores media keys.
  3. Start from pull Home; show why 186k QPS cannot merge 200 timelines.
  4. Hybrid fan-out: push ordinary authors, pull celebrities.
  5. Inboxes hold post ids; hydrate and re-check privacy.
  6. Outbox so a committed post is not lost to Kafka; fan-out is idempotent.
  7. Stories use a TTL tray, not the main feed inbox.
  8. Likes are unique pairs; counts are eventual.
  9. CDN for bytes; never stream images through the post service.
  10. 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.