Vibe Engines
YouTube
System Design

Design Instagram

Step 1 / 9

Learn system design by building a photo-sharing app like Instagram step by step.

The numbers to beatimmutableimages~95%+from edgelongTTL

The whole design, in writing

Learn system design by building a photo-sharing app like Instagram step by step. An interactive guide covering media upload, blob storage and CDN delivery, image processing, the timeline feed, like/view counters, ephemeral stories, and sharding for scale.

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What is Instagram?

Upload a photo; billions of people scroll endless photos. It sounds like a feed, but the twist is media: images are big, immutable, and viewed far more than they’re posted — so the bytes, not the rows, dominate the design.

Userpost / scroll
New in this step: User.

Two big moves: keep heavy media out of the database (store pixels in a blob store + CDN, keep only pointers in the DB), and precompute feeds so scrolling is a cheap lookup. Everything else builds on those.

What the new pieces do

Userclient
Uploads photos and, far more often, scrolls a feed of them. The app is read-heavy and media-heavy — both shape every decision here.

Step 1 · The skeleton

Bytes here, rows there

A post is two very different things: a big image and a little bit of structured data (caption, author, timestamp). Cram the image into the database and it bloats, slows, and gets expensive fast.

API GatewayObject StoreMetadata DB
New in this step: API Gateway, Object Store, Metadata DB. · swipe to pan the diagram

A post = a big image + a little caption/author/timestamp. Where do the bytes go?

  1. A multi-MB binary in every row bloats the DB, wrecks backups, and makes queries crawl. Databases are tuned for small structured rows, not megabytes of pixels.

  2. Base64 inflates size ~33% and still drags the binary through your query layer and caches. You’ve made the bytes problem bigger and slower.

  3. Each store does what it’s best at — the blob store holds cheap durable bytes, the DB holds small queryable rows pointing at them. They scale and cost out independently.

Split them. The API Gateway stores the image in an Object Store and writes a row to the Metadata DB holding the caption and the image’s location — a pointer, not the pixels. Each store does what it’s best at.

What the new pieces do

API Gatewaybackend
The single entry point for posting and browsing. Routes uploads to storage and feed requests to the timeline, behind a load balancer.
Object Storestore
Cheap, durable blob storage holding the actual image bytes. The database stores only a pointer to the object, never the pixels.
Metadata DBstore
Posts, captions, the follow graph and image locations. Small rows, queried constantly — the structured backbone behind the media.

Step 2 · Serve images fast

Push photos to the edge

Serving every image from a central object store adds cross-region latency to every photo and hammers one place. In a media app, slow images are a slow app.

API GatewayCDNObject StoreMetadata DB
New in this step: CDN. · swipe to pan the diagram

Hundreds of millions of viewers need the same immutable images. How do you serve them?

  1. One origin adds cross-region latency to every photo and becomes a bandwidth choke point. Object stores are for durability, not global low-latency serving.

  2. You’d copy petabytes of cold photos nobody in that region views. You only need the hot slice near each viewer — that’s exactly what a cache is for.

  3. Immutable images cache at the edge with near-infinite TTL — the first viewer pulls from origin, everyone nearby is served from the edge. ~95%+ of bytes never touch origin.

Put a CDN in front of the object store. The first request for an image pulls it from the store; the edge then serves everyone nearby. Since an uploaded image never changes, it caches at the edge with a very long TTL.

  • immutableimages
  • ~95%+from edge
  • longTTL

What the new pieces do

CDNcache
Serves photos from servers near the viewer. Images are immutable once uploaded, so they cache at the edge essentially forever.

Back of the envelope

1 photo → ~4 stored sizes
thumb, feed, detail, original
immutable ⇒ TTL = forever
no edits means no invalidation — the hardest cache problem vanishes
~95%+ served from edge
origin sees only the long-tail miss traffic

Step 3 · One photo, many sizes

Process uploads async

A phone shouldn’t download a 12-megapixel original to show a 150px thumbnail. But generating every size during upload would make posting slow and block the user on heavy CPU work.

CDNedge imagesImage Processingthumbnails · variantsObject Storephotos · S3Metadata DBposts · users
New in this step: Image Processing.

A 12MP original is too heavy for a 150px thumbnail. When do you make the other sizes?

  1. You block the user on heavy CPU for every size — posting feels slow and the upload path can’t scale past your encoder fleet.

  2. The post appears instantly; thumbnails and variants finish a beat later, off the request path. Heavy CPU work scales on its own fleet.

  3. On-the-fly transforms redo work for every cache miss and couple your CDN to image logic. Precomputing a few fixed sizes once is far cheaper at this scale.

After storing the original, hand off to Image Processing workers that generate thumbnails and display variants asynchronously and write them back to the blob store. The post appears immediately; its optimized sizes finish a beat later.

What the new pieces do

Image Processingworker
Asynchronously generates the many sizes and formats each photo needs (feed, thumbnail, full), so the device downloads exactly what it’ll display.

Step 4 · The feed

Precompute the timeline

Building a feed by querying every account a user follows, every time they refresh, is far too slow — reads are the hottest path in the whole app.

API Gatewayupload + feedFeed Cacheprecomputed timeline
New in this step: Feed Cache.

Reads dominate. How do you build a user’s feed on refresh?

  1. That’s the hottest path in the app doing a huge fan-in read every scroll. It collapses under load — feeds must be cheap to read.

  2. Do the expensive merge once when someone posts, not on every scroll. Opening the app becomes a single cheap cache read.

  3. Feeds are personalized, ranked and constantly changing — a rendered blob is stale instantly and impossible to update granularly. Cache post IDs, not the rendered page.

Maintain a Feed Cache: a precomputed list of recent post IDs per user, updated by fan-out when people they follow post. Opening the app reads that list and hydrates image URLs from the CDN — fast and cheap.

What the new pieces do

Feed Cachecache
A ready-made list of post IDs per user so opening the app is one fast lookup, not a live query across everyone they follow.

Step 5 · Likes at scale

Counters the async way

A viral post can take millions of likes in minutes. Doing a synchronous UPDATE count = count + 1 per tap creates brutal write contention on one row and can stall everything.

Image Processingthumbnails · variantsCounterslikes · viewsEventsfan-out · counts
New in this step: Counters, Events.

A viral post takes millions of likes in minutes. How do you count them?

  1. Every tap contends on one row — the classic hot-row write storm. That single post becomes a global lock and everything queues behind it.

  2. Counting millions of rows on every view is even worse than the write. You need a maintained running total, not a scan.

  3. The tap returns instantly (optimistic bump); the authoritative total is aggregated from the stream into sharded counters that spread the hot key. A sliver of staleness buys surviving virality.

Emit likes as events onto a stream and fold them into Counters asynchronously (sharded/approximate counters for the hottest posts). The user sees an instant optimistic bump; the authoritative total catches up moments later.

What the new pieces do

Countersstore
Aggregated like and view counts, updated asynchronously so a viral post’s flood of taps never slows posting or reading.
Eventsbus
A stream of posts and likes. Feed fan-out, counters and analytics each consume it independently and asynchronously.

Back of the envelope

1 viral post × millions/min
one row = one hot key, the write bottleneck
shard the counter into N parts
each tap hits part = hash(user) % N, summed on read
optimistic UI + async fold
user sees +1 now; the true total converges in seconds

Step 6 · Here then gone

Stories & ephemerality

Stories vanish after 24 hours. Sweeping the database for expired posts and pulling them from every feed would be a constant, expensive chore.

API Gatewayupload + feedStoriesephemeral · TTL
New in this step: Stories.

Stories vanish after 24h. How do you make old ones disappear?

  1. A sweep over billions of rows is a constant expensive chore, and stories linger until the job runs. Expiry should be automatic, not scheduled.

  2. Lifetime becomes a property of the data — the store reclaims it for free and feeds just stop showing it. The same trick powers sessions and OTPs.

  3. Soft-deletes pile up unbounded, bloat every query with a “not deleted” filter, and waste storage on data no one can ever see again.

Give each story a TTL so it auto-expires in storage and the cache; feeds simply stop showing it once it’s gone. No cleanup job — expiry is a property of the data, and the system reclaims it for free.

What the new pieces do

Storiesservice
Posts that vanish after 24h. A TTL on each story auto-expires it, so storage and feeds clean themselves up without a sweep.

Step 7 · Scale & the sharp edges

Shard everything

Billions of users and posts overflow any single database, and celebrity accounts create hotspots in both the feed fan-out and the counters.

UserAPI GatewayCDNFeed CacheImage ProcessingObject StoreMetadata DBCountersStoriesEvents
The system as it stands at this step. · swipe to pan the diagram

Billions of users overflow one DB, and celebrities create hotspots. What’s the plan?

  1. Replicas help reads, but every write still funnels to one primary that eventually overflows. Vertical scaling has a ceiling; sharding doesn’t.

  2. Hash-sharding spreads the median load across many shards; the handful of extreme accounts get bespoke handling (pull not push, split counters) so no single shard or row is the bottleneck.

  3. Geo-sharding skews badly — follows and virality cross regions freely, creating cross-shard reads and lopsided load. Hash-by-id spreads more evenly for a global social graph.

Shard the Metadata DB (by user/post id) behind the gateway and load-balance the stateless services. Handle celebrities with the hybrid feed (pull, not push) and hot-key counter sharding, so no single row or shard becomes the bottleneck.

You did it

You just designed Instagram.

UserAPI GatewayCDNFeed CacheImage ProcessingObject StoreMetadata DBCountersStoriesEvents
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • Split media from metadata: pixels in a blob store, a pointer in the DB.
  • A CDN serves immutable images from the edge, so media loads fast.
  • Async image processing makes the many sizes without slowing uploads.
  • A precomputed feed cache turns scrolling into a cheap lookup.
  • Likes/views aggregated asynchronously via events and sharded counters.
  • Stories expire automatically via TTLs — no cleanup job needed.
  • Sharding plus hybrid feeds and hot-key handling absorb celebrity scale.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. How does ranking fit in?

    The feed cache holds candidate post IDs; a ranking service scores them (recency, affinity, predicted engagement) at read time over that small candidate set. Keeping ranking off the write path lets you change the model without rebuilding everyone’s feed.

  2. A user with 50M followers posts — does fan-out-on-write work?

    No — writing one post into 50M feed caches is a write storm. Celebrities use the pull/hybrid model: their posts are fetched at read time and merged in, so a single post never fans out to millions.

  3. Are like counts exactly correct?

    They’re eventually consistent. Like events are aggregated asynchronously and the hottest counters are sharded/approximate, so the number can lag a few seconds. That’s a fine trade for likes — you don’t need billing-grade exactness.

  4. Where does NSFW / moderation run?

    On the async processing path, alongside resizing — a classifier runs after upload and before wide distribution. Posting stays fast while flagged media can be held or limited before it reaches feeds.

  5. How do you stop one user seeing another’s private photos via the CDN?

    Private media is served through signed, expiring URLs (or token-checked edge auth), so a CDN path alone isn’t enough — the link only works for an authorized viewer for a short window.

Check yourself — the answers, and why

Eight steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Image pixels live in a blob store, not the database, because databases are…

    • Cheaper per GB
    • Bad at giant binaries, great at small rows
    • Slower to back up

    DBs are tuned for small queryable rows; blobs belong in object storage with a pointer in the row.

  2. Instagram images cache at the edge with a near-infinite TTL because they’re…

    • Small
    • Immutable
    • Encrypted

    An uploaded image never changes, so there’s nothing to invalidate — the hardest cache problem disappears.

  3. The many thumbnail sizes are generated…

    • Synchronously during upload
    • Asynchronously in workers after storing the original
    • On the CDN per request

    Heavy CPU work stays off the upload path so posting feels instant.

  4. A precomputed feed cache makes scrolling…

    • A live fan-in query every refresh
    • A cheap read of a maintained post-ID list
    • A full-page HTML cache

    Fan-out-on-write does the merge once at post time; reads become a single cheap lookup.

  5. Millions of likes/minute survive because counts are…

    • Updated synchronously per tap
    • Aggregated async into sharded counters
    • Recounted on every view

    A hot single row can’t take a viral storm; sharded async counters can.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Post: upload a photo with a caption.
  • Scroll: open the app to a fast feed of posts from people you follow.
  • Serve media: load images quickly for hundreds of millions of viewers.
  • Like & view: count taps on a post, even a viral one.
  • Stories: ephemeral posts that vanish after 24h.

The qualities that shape everything

Each one names the mechanism that buys it.

Heavy media without bloating the database
Store pixels in an object store and keep only a pointer + caption row in the DB, so each store scales and costs out on what it’s best at.
Images load fast everywhere
A CDN caches immutable images at the edge with a near-infinite TTL, so ~95%+ of bytes are served near the viewer and origin stays nearly idle.
Posting stays snappy
Store the original, then generate thumbnails and variants asynchronously in a worker fleet, so heavy CPU work never blocks the upload path.
Scrolling is a cheap read
Precompute a per-user list of recent post IDs via fan-out on write, so opening the app is one cache lookup, not a live fan-in across everyone you follow.
Millions of likes a minute survive
Emit likes as events and fold them into sharded/approximate counters asynchronously, with an optimistic bump on the tap — no hot-row write storm.
Billions of users and celebrity hotspots
Shard the metadata DB by id and handle extreme accounts specially — pull-based hybrid feeds and hot-key counter sharding — so no single row or shard is the bottleneck.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Pixels in a blob store, pointer in the DB over the image as a BLOB column

A multi-MB binary in every row bloats the database, wrecks backups and makes queries crawl. Databases are tuned for small structured rows; blob storage holds cheap durable bytes, and a pointer links the two.

A CDN edge cache over serving every image from origin

One central store adds cross-region latency to every photo and becomes a bandwidth choke point. Immutable images cache at the edge with a very long TTL — the first viewer pulls origin, everyone nearby is served locally.

Resize asynchronously in workers over resizing during upload

Generating every size before responding blocks the user on heavy CPU and can’t scale past the encoder fleet. Store the original and let workers finish the variants a beat later, off the request path.

Fan-out on write over a live fan-in query per refresh

Merging every followed account on every scroll is the hottest path doing a huge read — it collapses under load. Do the merge once at post time so opening the app is a single cheap cache read.

Async, sharded counters over a synchronous UPDATE per tap

Every tap contending on one post row is a hot-row write storm that turns a viral post into a global lock. Fold like events into sharded counters asynchronously — a sliver of staleness for surviving virality.

What this teaches

Learn system design by building a photo-sharing app like Instagram step by step. An interactive guide covering media upload, blob storage and CDN delivery, image processing, the timeline feed, like/view counters, ephemeral stories, and sharding for scale.

Key takeaways

  • Split media from metadata: pixels in a blob store, a pointer in the DB.
  • A CDN serves immutable images from the edge, so media loads fast.
  • Async image processing makes the many sizes without slowing uploads.
  • A precomputed feed cache turns scrolling into a cheap lookup.
  • Likes/views aggregated asynchronously via events and sharded counters.
  • Stories expire automatically via TTLs — no cleanup job needed.
  • Sharding plus hybrid feeds and hot-key handling absorb celebrity scale.

Concepts covered

  • What is Instagram?
  • Bytes here, rows there
  • Push photos to the edge
  • Process uploads async
  • Precompute the timeline
  • Counters the async way
  • Stories & ephemerality
  • Shard everything
built to be posted, not memorized — make the calls, kill the CDN, run the gauntlet.
Finished this one? 0 / 65 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs