Design Instagram: System Design & Architecture

Build a photo-first social network: users post photos and short videos to their followers, browse a home feed, and share stories that vanish after 24 hours. The feed is the Design a Newsfeed problem wearing a media body - the genuinely new organ is content with an expiration date.

Scope

In Scope

  • Posts: photos and short videos with captions, processed into multiple resolutions and served via CDN
  • Follow graph and a chronological home feed of followed accounts (importing the Design a Newsfeed machinery)
  • Stories: posts that are visible for exactly 24 hours and then disappear, with a tray of accounts that have active stories
  • Story view tracking: the author sees who viewed and how many
  • Likes and flat comments on posts; profile pages with a grid of the user's posts

Out of Scope

  • Ranked/algorithmic feed and Explore recommendations: ML systems in their own right - we ship chronological, for exactly the reasons argued in Design a Newsfeed
  • Direct messages: that is Design WhatsApp, whole and entire
  • Search (accounts, hashtags, places): a search-engine design of its own
  • Reels as a full video platform: the video pipeline is Design YouTube; we support short video posts with a slim version of it
  • Content moderation, ads, and creator monetization
  • Live streaming

💡 This design deliberately stands on two others

Instagram is mostly a composition of systems this practice set already covers. The follow graph, feed fan-out, celebrity problem, likes, and comments are Design a Newsfeed, imported wholesale. Video processing is Design YouTube, imported slim. Rebuilding those here would waste the interview. What is genuinely new - and where this page spends its depth - is two things. First, the image pipeline: photos have their own processing concerns (resolution ladders for different screens, EXIF stripping for privacy, placeholder generation for perceived speed) that are lighter than video but not trivial. Second, and most distinctive: ephemeral content. Stories introduce a problem no other design in this set has - content with a hard expiration date, where "gone after 24 hours" is a promise the whole architecture has to keep, from the database to the CDN. Saying this out loud in an interview is a strength, not a dodge: "the feed here is the standard newsfeed design - let me focus on what is different about Instagram" shows you recognize composed systems and know where the novel work lives.

Functional Requirements

  • A user can post photos or short videos with a caption, visible to their followers
  • A user can follow and unfollow accounts, and sees a home feed of followed accounts' posts, newest first
  • A user can post a story, visible to followers for exactly 24 hours and never after
  • A user sees a tray of followed accounts with active stories and can tap through them
  • A story's author can see who viewed it and the view count; nobody else can
  • A user can like posts and leave flat comments
  • A user can open any profile and see a grid of that account's posts
  • Photos feel instant: a placeholder renders immediately and the right resolution loads for the screen

Non-functional Requirements

  • Feed load: p99 < 200ms for the feed API; media bytes come from the CDN separately
  • Post visibility: a new post reaches followers' feeds within seconds (the Newsfeed eventual-consistency window)
  • Story expiry is sharp: a story is NEVER visible past its 24 hours, on any surface, including cached copies - though physical deletion of the bytes may lag by hours
  • Image processing: a photo post is published within seconds; short videos within a couple of minutes
  • Scale: ~2B monthly users, ~500M daily actives, ~100M posts/day, ~200M stories/day
  • Durability: posts are forever (never lost); stories are deliberately NOT durable beyond their window - deletion is a feature with a deadline

💡 What does "expires at 24 hours" actually require? Logical vs physical expiry

The expiry promise hides two different requirements that are worth separating, because they get opposite treatments. Logical expiry is the correctness promise: at hour 24, no user can see the story anywhere. This must be sharp and instant - and the only way to make it sharp is to enforce it at read time. Every query that could surface a story filters on expires_at > now. That check costs nothing, works the moment the clock passes, and does not depend on any cleanup job having run. If all the deletion machinery in the system crashed for a day, stories would still vanish on time, because visibility never depended on deletion. Physical expiry is the economics: the story's rows and media bytes should actually be deleted, reclaiming storage. This can be lazy - a sweeper that runs hourly, batch-deleting expired records and letting a storage lifecycle rule remove the bytes. Nobody can observe whether cleanup ran at hour 24 or hour 30, so it is allowed to be cheap and batched. The subtle third surface is caches and the CDN: a copy cached at hour 23 must not be servable at hour 25. The elegant fix is to make expiry a property of the URL itself - story media URLs are signed with an expiration equal to the story's expires_at, so the CDN mechanically refuses them the moment the story dies, with no purge required. One sentence to carry out of this: correctness at read time, economics in batch. Design URL Shortener made the same split for expired links - the 404 is instant, the cleanup is scheduled.

Back-of-the-Envelope Estimates

  • Assume ~2B monthly active users, ~500M daily actives
  • A write is a post, story, like, comment, follow, or story-view event. A read is a feed load, story tray load, profile view, or media fetch
  • Posts: ~100M/day ÷ 86,400 ≈ 1.2K posts/sec average, ~12K/sec peak
  • Post media: ~100M posts/day x ~3MB (original + rendition ladder) ≈ ~300TB/day of permanent media - accumulating forever, ~110PB/year, served via CDN (the Design YouTube economics at photo scale)
  • Stories: ~200M/day x ~2MB ≈ ~400TB/day of inflow - BUT stories live 24 hours, so steady-state story storage is just one day's window: a constant ~400TB, whether the product runs for a month or a decade. Ephemerality turns a growth curve into a flat line
  • Feed fan-out: ~200 avg followers x 100M posts/day = ~20B feed deliveries/day ≈ 230K/sec average - the Design a Newsfeed write-amplification problem at ~5x the volume, which makes the hybrid push/pull approach mandatory, not optional
  • Story views: ~500M story viewers x ~30 stories/day ≈ 15B view events/day ≈ 175K/sec average - story views out-write everything else in the system, and every one of those rows self-deletes within a day
  • Feed reads: ~5B feed loads/day ≈ 60K/sec average - read-heavy at the API, write-heavy under fan-out, exactly the Newsfeed shape

💡 What the estimates mean: a tale of two content types

The numbers split Instagram's content into two species with opposite storage stories, and the contrast is the most useful thing on this page. Posts accumulate. ~300TB/day of media, forever - a growth curve that reaches ~110PB/year. Everything Design YouTube concluded about permanent media applies at photo scale: bytes go to object storage behind a CDN, popularity skew makes edge caching absorb most reads, old cold media tiers down to cheaper storage classes, and the database holds only metadata and pointers (the Design Dropbox bytes-versus-metadata split, third appearance). Stories evaporate. ~400TB/day flows in, but the 24-hour lifetime means steady-state storage is one day's window - a constant ~400TB no matter how many years the product runs. Ephemerality is not just a product feature; it is a storage architecture: the entire stories system, including its 15B view events a day, is self-cleaning by construction. Every stories row and blob carries its own death date, which is why the data model leans on TTLs, expiry-day indexes (the Design URL Shortener cleanup pattern), and storage lifecycle rules rather than ever-growing tables. The graph, feed, likes, and comments are the Design a Newsfeed stack verbatim - NoSQL feed pointers, hybrid celebrity fan-out, buffered counters - at roughly 5x the volume, which changes the thresholds but not the design. We do not re-derive any of it here. So the database story is: import the Newsfeed stack for the social skeleton, add object storage + CDN for the media body (YouTube economics), and give stories their own TTL-native corner where everything expires. Three regimes, chosen by content lifetime.

High-level Architecture

  • API Gateway: auth, rate limiting on the write paths (posting, following, story views)
  • Media Pipeline: presigned direct upload of originals, then an image processor that strips EXIF, builds the resolution ladder, and generates blurhash placeholders; a slim video path (Design YouTube, shortened) handles video posts
  • Object Storage + CDN: permanent post media tiered by popularity; story media under lifecycle rules that delete bytes after expiry
  • Post Service + Fan-out + Feed Service: the Design a Newsfeed machinery, imported - pointer fan-out for regular accounts, pull-and-merge for celebrities, feed hydration attaching CDN URLs
  • Follow (Social Graph) Service: who follows whom, with the reverse index fan-out needs (Newsfeed, unchanged)
  • Story Service: story records stamped with expires_at, per-author active-story lists, the tray builder, and read-time expiry filtering on every path
  • Expiry machinery: a sweeper walking the expiry-day index to batch-delete dead stories, plus storage lifecycle rules for the bytes - lazy on purpose, because visibility never depends on it
  • Story View Pipeline: view beacons → idempotent view rows (partitioned by story, TTL-cleaned) → buffered counters (the Newsfeed counter pattern)

Data/User Flow Diagram

POST - permanent content (the Newsfeed write path + media)
[Client] --presigned upload--> [Object Storage: original]
                                      |
                                      v
                            [Image Processor]
                       strip EXIF | resolution ladder
                       1080 / 720 / 320 | blurhash
                                      |
                                      v
                     [Object Storage: renditions] --> [CDN]
[Client] --> [Post Service] --> [Posts DB] --> [Fan-out]
              (from here, exactly Design a Newsfeed:
               pointers pushed, celebrities pulled)

STORY - ephemeral content (the new organ)
[Client] --> [Story Service] --> [Stories DB: expires_at = now + 24h]
                   |                   (expiry-day index for the sweeper)
                   +--> [author's active-story list (Redis, TTL)]
              media URLs are SIGNED TO DIE AT expires_at -> CDN
              cannot serve a dead story even from cache

STORY TRAY - read path (pull, not push)
[Client] --> [Story Service] --> following list
                   --> per-author active lists (shared, cached hard)
                   --> filter expires_at > now   <- THE expiry guarantee

CLEANUP - lazy, invisible, interruptible
[Sweeper (hourly)] --> expiry-day index --> batch-delete rows
[Storage lifecycle rule] --> deletes story bytes after expiry

💡 Why the story tray is pull-based when the feed is push-based

The feed and the story tray ask the same fan-out question - how does content from accounts you follow reach you? - and get opposite answers. Working out why is one of the best exercises in this set. The feed (per Design a Newsfeed) is push: when someone posts, pointers are written into each follower's feed ahead of time, because a feed is a per-user merged timeline read constantly - precomputing it makes millions of reads cheap. The tray is pull. When you open the app, the Story Service takes your following list and checks each account's active-story list - a small per-AUTHOR structure, not a per-viewer one. Two properties flip the answer. First, the read shape: the tray shows authors, not a merged timeline, and every one of an author's followers reads the exact same active list - one cached object serves millions of viewers, the same shared-read economics as the celebrity pull path in Newsfeed. Second, the lifetime: pushing story pointers into millions of trays would write fan-out amplification for content that self-destructs tomorrow - maximum write cost for minimum useful life. The transferable lesson: push versus pull is not a property of your app - it is a property of each content type, decided by read shape and content lifetime. One product, two fan-out answers, both right.

Data Model & Storage

  • Posts, Follow Graph, Feed Store, Reactions, Comments: the Design a Newsfeed schemas, unchanged - the one addition is that a post row carries its media renditions (URLs per resolution plus the blurhash string).
  • Media (Object Storage + CDN): post originals and rendition ladders, tiered hot-to-cold by age and popularity; story media in a separate prefix governed by a lifecycle rule that deletes bytes after expiry.
  • Stories (NoSQL, partitioned by author_id): story_id (time-ordered), media refs, created_at, expires_at, view_count (denormalized). A secondary index buckets stories by expiry day - the Design URL Shortener expiration-index pattern - so the sweeper reads "today's dead stories" without scanning anything.
  • Active-story lists (Redis, one small list per author, TTL = latest story's expiry): the shared object the tray is built from; rebuildable from the Stories DB on a cache miss, so losing Redis loses nothing.
  • Story Views (NoSQL, partitioned by story_id, one row per viewer): idempotent by key (story, viewer), readable only by the author, and stamped with a TTL just past the story's expiry - 15B rows/day that clean up after themselves.
  • View counters (Redis buffers): the Newsfeed buffered-counter pattern, flushed to the story row every few seconds.

💡 What ephemerality buys: the storage math

Compare the two content types at ten years of operation. Posts: ~300TB/day, accumulating. Year one ends near 110PB; year ten ends near 1.1EB. Every byte ever posted is still there, still replicated, still costing money - which is why permanent media needs the whole YouTube toolkit: popularity tiering, cold storage classes, CDN economics. Stories: ~400TB/day of inflow, but steady-state storage is capped at one day's window - roughly 400TB in year one, and still roughly 400TB in year ten. The system's busiest write surface (200M stories plus 15B view events a day) contributes zero long-term growth. Deletion with a deadline turned a compounding cost into a constant. This is the same insight as Design URL Shortener's 1-year link expiry - TTLs cap the table - taken to its extreme: there, expiry was housekeeping; here, it is the product itself, and the architecture (TTL attributes, expiry-day indexes, lifecycle rules, self-deleting view rows) is built so that everything about a story carries its own death date from the moment it is written. Nothing needs to remember to clean up, because nothing was ever going to live. One honest caveat: Instagram's real product quietly archives your expired stories for you (visible only to you). That flips the storage math back to accumulating - a reminder that a single product checkbox ("archive my stories") can undo an architecture's neatest property, and worth mentioning in an interview as the kind of product-engineering negotiation that actually happens.

API Design

POST /v1/posts
Create a post (caption + media refs from a prior presigned upload). The feed side behaves exactly as Design a Newsfeed.
GET /v1/feed?cursor=...
Chronological home feed, hydrated with CDN URLs and blurhash placeholders (Newsfeed, imported).
POST /v1/users/:id/follow
Follow/unfollow (Newsfeed, imported).
POST /v1/stories
Post a story; returns expires_at and media URLs signed to die with the story.
GET /v1/stories/tray
Followed accounts with active stories, unseen-first.
GET /v1/stories/user/:userId
A user's active stories, in order - every response filtered by expires_at > now.
POST /v1/stories/:id/views
View beacon; idempotent per viewer.
GET /v1/stories/:id/viewers?cursor=...
Who viewed - author only.
POST /v1/posts/:id/likes | /comments
Likes and flat comments (Newsfeed, imported).

Low-level Design

Image pipeline - what happens to a photo between upload and feed (step by step):

  • 1) The client uploads the original straight to object storage via a presigned URL (the Design Dropbox flow - our servers never touch the bytes).
  • 2) The processor validates the file is really an image, then strips EXIF metadata - the privacy step: phone cameras embed GPS coordinates in every photo, and serving originals would leak the location of everyone's home.
  • 3) It renders the resolution ladder (1080 for full-screen, 720 for feed, 320 for grid thumbnails) in modern formats (WebP/AVIF, 30-50% smaller than JPEG), plus a blurhash - a ~30-character string encoding a blurred preview.
  • 4) Renditions land in object storage, the post publishes, and fan-out proceeds per Design a Newsfeed.
  • Seconds, not minutes: unlike video there is no transcoding ladder measured in compute-hours and no manifest - an image is a single fetch, so the whole YouTube segment/manifest apparatus simply does not apply. Video posts take the slim version of the YouTube pipeline: 2-3 renditions, short durations, same queue-and-workers shape.

Feed and fan-out - imported, not re-derived:

  • Posting, pointer fan-out to follower feed partitions, the hot-account pull path, cursor pagination, buffered like-counters, flat comments: all of it is Design a Newsfeed, and none of it changes here.
  • What scale changes: at ~20B feed deliveries/day (5x the Newsfeed estimates), the celebrity hybrid is mandatory and the hot-account threshold sits lower.
  • What media changes: feed hydration attaches each post's rendition URLs and blurhash, so the client renders the blurred placeholder instantly and swaps in the right resolution for its screen - the feed FEELS fast even before a single image byte arrives.
  • In an interview, spend one minute here and say where the depth lives: "this is the newsfeed design; the interesting deltas are media and stories."

Story lifecycle - from post to disappearance (the genuinely new part, step by step):

  • 1) Media processes through the same pipeline (EXIF stripped, ladder built).
  • 2) The Story Service writes the story row with expires_at = now + 24h, including its expiry-day index entry - the row is born carrying its own death date.
  • 3) The author's active-story list in Redis gets the story appended, its TTL extended to the latest expiry. Followers' trays light up on their next load.
  • 4) Media URLs handed to any client are signed with expiry equal to the story's expires_at - so every cached copy, including the CDN's, mechanically stops working at hour 24 with no purge fanned out.
  • 5) At hour 24: nothing happens, and that is the design. Every read path already filters expires_at > now, so the story is instantly invisible everywhere.
  • 6) Within the next hours, lazily: the sweeper reads the expiry-day bucket, batch-deletes rows, and the storage lifecycle rule removes the bytes. View rows TTL away on their own.
  • The story was never deleted "on time" - it was ignored on time, and deleted whenever convenient.

Story tray - building it with pulls (step by step):

  • 1) Client requests the tray; the service loads the user's following list (cached, per Newsfeed).
  • 2) For each followed account, check the per-author active-story list in Redis - one small shared object per author, so a celebrity's list is read by millions but computed once.
  • 3) Misses rebuild from the Stories DB (query the author's partition for expires_at > now) and re-cache - Redis can be lost wholesale without losing a story.
  • 4) Filter dead stories, order the tray (unseen authors first, using the viewer's per-author seen marker), return.
  • 5) The whole tray response is itself cached for ~60 seconds per user - staleness tolerance buys another 10x.
  • Why this is pull when the feed is push: shared per-author read shape plus 24-hour lifetime - argued fully in the architecture note, and worth restating in the interview as "fan-out strategy is per content type, not per app."

Story views - 15B self-deleting writes a day (step by step):

  • 1) Opening a story fires a view beacon.
  • 2) One row per (story_id, viewer_id) - idempotent by key, so rewatching does not double-count, same move as Tinder's swipe rows.
  • 3) The count on the story row is a buffered counter (the Newsfeed pattern): increments buffer in Redis, flush every few seconds, display approximate.
  • 4) The viewer LIST is author-only, cursor-paginated from the story's partition - a celebrity story accumulating 20M viewer rows in a day is a write storm, but a bounded one: the story dies in 24 hours, and every view row carries a TTL just past it.
  • 5) The entire subsystem is self-cleaning: its tables are always roughly one day large, no matter how many years it runs.
  • The contrast to name: view events out-write every other surface in the app (175K/sec average), and yet they are the least dangerous data in the system - because nothing that expires can accumulate into a problem.

Database Types and Schemas

Schemas (Posts / Feed / Graph / Comments: unchanged from Design a
Newsfeed - shown here only where Instagram differs)

- Posts (Newsfeed schema + media)
  ...all Newsfeed fields...
  media JSONB:
    [{ "type": "image",
       "renditions": { "1080": "url", "720": "url", "320": "url" },
       "blurhash": "LEHV6nWB2yk8pyo0adR*.7kCMdnj" }]

- Stories (NoSQL, e.g. DynamoDB)
  PK: author_id, SK: story_id (time-ordered)
  Attributes: media_refs, created_at, expires_at, view_count
  TTL attribute: expires_at + grace  -- rows self-delete
  GSI (the sweeper's index): PK: expiry_day ("2026-07-06"), SK: story_id
    -- "today's dead stories" as one bucket read: the Design URL Shortener
    --  expiration-index pattern, verbatim
  -- EVERY read of this table filters expires_at > now; visibility never
  -- waits for deletion

- Story Views (NoSQL)
  PK: story_id, SK: viewer_id      -- one row per viewer: idempotent
  Attributes: viewed_at
  TTL: story expires_at + grace    -- 15B rows/day, all self-deleting
  -- readable only via the author-only viewers endpoint

Object storage layout
  /media/posts/{post_id}/{1080,720,320}.webp     -- permanent, tiered by age
  /media/stories/{story_id}/*                    -- lifecycle rule: delete
                                                 --  after expiry
  -- story URLs are presigned with expiry = the story's expires_at:
  --  the CDN refuses them at hour 24 automatically, no purge needed

Redis keys
- Active stories per author (the tray's building block)
  "active:{author_id}" -> [ {story_id, expires_at}, ... ]
  TTL: latest expires_at   -- rebuildable from Stories DB on miss
- Tray cache
  "tray:{user_id}" -> rendered tray, TTL ~60s
- Story view counters (Newsfeed buffered-counter pattern)
  "sviews:{story_id}" -> pending increments, flushed every few seconds
- Seen markers (orders the tray)
  "seen:{user_id}:{author_id}" -> last seen story_id, TTL 48h

API Request/Response Payloads

POST /v1/posts
Request
{
  "caption": "Golden hour",
  "media_refs": ["upload_8f3e2d1c"]
}
Response
{
  "post_id": "p_9a1b44",
  "media": [
    {
      "type": "image",
      "renditions": {
        "1080": "https://cdn.example.com/media/posts/p_9a1b44/1080.webp",
        "720":  "https://cdn.example.com/media/posts/p_9a1b44/720.webp",
        "320":  "https://cdn.example.com/media/posts/p_9a1b44/320.webp"
      },
      "blurhash": "LEHV6nWB2yk8pyo0adR*.7kCMdnj"
    }
  ],
  "created_at": "2026-07-06T18:04:11Z"
}

// The original was uploaded via presigned URL and processed first (EXIF
// stripped, ladder rendered). The blurhash is ~30 characters that decode
// into a blurred preview - feeds render it instantly while real pixels
// load. Fan-out to followers proceeds per Design a Newsfeed.
POST /v1/stories
Request
{
  "media_ref": "upload_7a2b8f4e"
}
Response
{
  "story_id": "s_4c8e6f2a",
  "expires_at": "2026-07-07T18:04:11Z",
  "media_url": "https://cdn.example.com/media/stories/s_4c8e6f2a/1080.webp?sig=...&exp=1751911451"
}

// Note exp in the URL equals the story's expires_at: every copy of this
// URL - including whatever the CDN cached - stops working at the same
// moment the story logically dies. Expiry travels WITH the content.
GET /v1/stories/tray
Response
{
  "tray": [
    { "author": { "user_id": "u_456", "name": "dana",
                  "avatar": "https://cdn.example.com/a/u_456.webp" },
      "active_stories": 3, "all_seen": false },
    { "author": { "user_id": "u_bigband", "name": "The Big Band",
                  "avatar": "https://cdn.example.com/a/u_bb.webp" },
      "active_stories": 1, "all_seen": true }
  ]
}

// Built by PULL: your following list x per-author active lists (one
// small shared cache object per author, read by all their followers).
// Unseen authors sort first. Every entry already passed the
// expires_at > now filter - the tray cannot show a dead story.
POST /v1/stories/:id/views and the author's viewer list
Request
{ "story_id": "s_4c8e6f2a" }
Response
// viewer's beacon response:
{ "ok": true }

// GET /v1/stories/s_4c8e6f2a/viewers (author only):
{
  "view_count": 18240,
  "viewers": [
    { "user_id": "u_789", "name": "sam",
      "viewed_at": "2026-07-06T19:12:40Z" }
  ],
  "next_cursor": "2026-07-06T19:12:40Z#u_789",
  "has_more": true
}

// One row per (story, viewer): rewatching never double-counts. The
// count is approximate (buffered counter, the Newsfeed pattern); the
// list is exact, author-only, and every row TTLs away with the story.
Error responses (examples)
Response
// 404 - story expired, deleted, or never existed: identical on purpose
{
  "error": "not_found",
  "message": "Story not found"
}
// An expired story is indistinguishable from one that never existed -
// "it was there an hour ago" leaks nothing now.

// 403 - viewer list is the author's alone
{
  "error": "forbidden",
  "message": "Only the story's author can view this"
}

💡 Three terms from the flows: blurhash, EXIF stripping, lifecycle rule

Blurhash: a tiny string (~30 characters) that encodes a heavily blurred version of an image, computed once at processing time and shipped inside the feed JSON itself. The client decodes it locally and paints the blur before any image byte arrives, so the feed never shows gray boxes. It costs almost nothing and is most of why media apps feel fast on bad connections - perceived speed is a design surface of its own. EXIF stripping: photos from phones carry embedded metadata - camera model, timestamp, and crucially GPS coordinates of where the photo was taken. Serving originals would hand every follower the location of your home, workplace, and kids' school. The processor strips all of it before any rendition is stored. This is a one-line step that is a genuine privacy requirement, and forgetting it is a classic real-world breach. Lifecycle rule: a policy attached to a storage bucket or prefix - "delete objects here N hours after creation" - executed by the storage system itself. Story bytes get cleaned up by the infrastructure, not by our code: no sweeper we write can forget, crash, or fall behind in a way that leaks storage forever. Delegating deletion to the platform is the physical-expiry counterpart of enforcing visibility at read time.

Concluding Discussion

Potential Bottlenecks

  • Issue: Celebrity post fan-out at ~20B deliveries/day - the Newsfeed write-amplification problem at 5x volume.
    Mitigation: the imported hybrid push/pull with a lower hot-account threshold; nothing new needed, which is itself the point of importing the design.
  • Issue: A celebrity story draws tens of millions of views in hours - a write storm on one story's view partition and counter.
    Mitigation: idempotent view rows spread within the story partition, buffered counters for the number, and the bound nobody else has: the storm is structurally over in 24 hours and its data self-deletes.
  • Issue: Photo-processing spikes (New Year's Eve) back up publishing.
    Mitigation: elastic workers on queue depth (images are seconds of CPU, so the fleet flexes fast), and under pressure publish with the essential renditions and backfill the rest - degrade the ladder, never the post.

Single Points of Failure

  • The expiry sweeper: if it dies, nothing user-visible happens - stories still vanish on time because visibility is enforced at read time, and signed URLs still die at the CDN. Only storage reclamation lags. This is the architecture's best property: correctness was never delegated to cleanup.
  • Redis (active lists, tray caches, counters): all rebuildable from the Stories DB; a cold restart costs a thundering rebuild, so cluster it and coalesce rebuilds per author.
  • The feed store and graph: same SPOFs and same mitigations as Design a Newsfeed - replicated, cache-fronted, rebuildable.

Security Improvements

  • EXIF/GPS stripping on every upload - the quiet privacy requirement that prevents photos from leaking home addresses.
  • Story media URLs signed to expire with the story: no cache, CDN, or saved link can outlive the content it points to.
  • Viewer lists are author-only, enforced server-side; view beacons are authenticated so view counts cannot be inflated by anonymous replay.
  • Expired story = 404 identical to never-existed; private accounts enforce the follow gate at hydration time, exactly as Newsfeed enforces follower-only visibility.

Critical Knowledge

If the interviewer asks "how do stories disappear at exactly 24 hours?":

  • Split the promise: logical expiry (nobody can see it - sharp, instant) versus physical expiry (bytes actually deleted - lazy, batched).
  • Logical: every read path filters expires_at > now, so the story is ignored on time even if every cleanup job is down; plus the elegant cache move - media URLs are signed to expire AT the story's expires_at, so CDN copies die mechanically with no purge.
  • Physical: a sweeper walks an expiry-day index (the Design URL Shortener cleanup pattern) batch-deleting rows, and a storage lifecycle rule removes bytes - hours late is fine, because nobody can observe it.
  • The line to say: correctness at read time, economics in batch.

If the interviewer asks "why is the story tray pull-based when the feed is push-based?":

  • Same fan-out question, two properties flip the answer. Read shape: the tray shows AUTHORS, and all of an author's followers read the same small active-story list - one cached object serves millions, the shared-read economics of Newsfeed's celebrity pull path made universal.
  • Lifetime: pushing story pointers into millions of per-user trays is maximum write amplification for content that self-destructs tomorrow.
  • So: feed = per-user merged timeline of permanent content → precompute (push); tray = shared per-author lists of dying content → read-time pull over caches.
  • The transferable lesson: push vs pull is decided per content type by read shape and lifetime - not once per app.

If the interviewer asks "what does the image pipeline do besides resizing?":

  • EXIF stripping - phone photos embed GPS coordinates, so serving originals leaks home addresses; stripping is a privacy requirement, not an optimization.
  • The resolution ladder (full-screen / feed / thumbnail) in modern formats (WebP/AVIF, 30-50% smaller) so each screen fetches only what it can show.
  • Blurhash placeholders - a 30-character blurred preview shipped inside the feed JSON, painting instantly while pixels load; perceived speed is its own design surface.
  • And the contrast with video: no manifest, no segments, no adaptive ladder - an image is one fetch, processing takes seconds not minutes, so the Design YouTube apparatus deliberately does not apply. Knowing when the heavier pattern is unnecessary is the skill.

If the interviewer asks "how do you handle 15 billion story views a day?":

  • One idempotent row per (story, viewer) - rewatching cannot double-count (the Tinder swipe-row move) - partitioned by story so celebrity storms spread within a partition.
  • The displayed count is a buffered counter (the Newsfeed pattern): increments buffer in Redis, flush every few seconds, approximate now, exact on reconciliation.
  • The viewer LIST is exact, author-only, cursor-paginated.
  • Then the property no other firehose in this set has: every view row carries a TTL just past its story's expiry, so the busiest write surface in the app (175K/sec average) holds a constant one day of data forever - what expires cannot accumulate into a problem.

If the interviewer asks "isn't Instagram just Newsfeed plus YouTube?":

  • Own it - mostly yes, and saying so is the strong answer: graph, feed fan-out, celebrity hybrid, counters, comments are Design a Newsfeed imported at 5x volume; video posts are Design YouTube, slimmed. Recognizing a composed system beats re-deriving its parts slowly.
  • Then name the genuine deltas and spend the interview there: the image pipeline's privacy and perceived-speed work (EXIF, blurhash), and above all ephemerality - content born with a death date, enforced at read time, in the URL signature, and in the storage lifecycle.
  • Close with the storage tale of two content types: posts compound (~110PB/year); stories flatline (~400TB steady state, forever) - ephemerality is a storage architecture, not just a product feature.
  • Bonus honesty: real Instagram archives your expired stories privately, flipping that math back - one product checkbox can undo an architecture's neatest property.