Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

System Design

Design Instagram

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

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.

Why this piece earns its place

Splitting a post across two systems means one post is now two writes with no transaction spanning them, and the interesting question is what happens when the second one fails. Write the row first and a crash before the upload lands leaves a post in someone’s feed pointing at bytes that were never stored — a permanent broken image a user can see. Write the bytes first and the same crash leaves an object nobody references, which costs storage and nothing else. So the order is bytes, then row, and the leftovers get swept: a job that finds objects older than some window with no matching row and reclaims them. Deletion runs the same way in reverse — drop the row, mark the object, let the sweeper unlink it — because the two stores can’t be made to commit together either. It’s worth saying out loud that the pointer is the only thing tying them together: nothing in the object store knows it belongs to a post, so if a metadata row is lost the bytes are unrecoverable in practice even though they are perfectly durable.

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.

Why this piece earns its place

A near-infinite TTL is only safe because the URL and the bytes are the same fact, and that stops being true the moment anything is edited in place. Re-crop a photo, replace a profile picture, or re-encode a variant and write it back to the same path, and every edge that already holds it keeps serving the old pixels until its TTL runs out — which we just said is effectively never. A global purge is the wrong tool here: it is slow, it is rate-limited, and it only works if you know every path involved. The fix is naming. Put a content hash or a version into the object key so a changed image is a different URL and the old one simply ages out unreferenced. That turns the hardest cache problem back into an ordinary write. It is also why the processing step should write each variant to its own derived key instead of overwriting anything — it preserves the property the CDN is leaning on. When that discipline slips the symptom is never an error; it is one user insisting their new photo is up and their friend still seeing the old one.

  • 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.

Why this piece earns its place

Moving the work off the request path creates a window I have to answer for: the post row exists and its variants don’t. If a feed asks for a display size that hasn’t been written yet, the honest choices are to serve the original and let the client scale it down, or to hold the post out of feeds until processing reports done. I’d do both — hold it out of other people’s feeds, and show authors their own original immediately, so the person who just posted never stares at a gap. Async also buys me a poison message. One photo that kills the encoder — a corrupt file, an unexpected animation, a frame size that blows the memory limit — comes back on the queue and kills the next worker too, and with unlimited retries a single upload can occupy the fleet. So retries are capped, the failure lands in a dead-letter queue for a human, and that post falls back to serving its original indefinitely rather than sitting invisible. Queue depth is the metric that matters here: a spike fails nothing, it just stretches the delay between posting and a sharp thumbnail, and no error rate will tell you.

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.

Why this piece earns its place

Fan-out on write is a knob, not a switch, and the two settings are who gets a list and how long it is. The write cost is followers times posts, and a large share of it is spent on people who won’t open the app today — an account dormant for months still collects every post from everyone it follows into a list nobody reads. So I’d fan out only to recently active followers and rebuild a dormant user’s feed on demand the first time they come back; it is the same merge, just paid once, by someone who is definitely there to see it. Length is the other direction. Keep the list short and it is cheap to write and cheap to hold, but a determined scroller reaches the end and drops into the slow path — on the hottest surface in the product, at the moment that person is most engaged. Keep it long and you pay to maintain pages most sessions never reach. I’d size it to cover a typical session comfortably and treat the fall-through as a real paginated query, not an edge case I can pretend away.

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.

Why this piece earns its place

What this step quietly bakes in is that a like is a number. It isn’t — it is two facts with different requirements. The total can be approximate and seconds late and nobody can tell. Whether I liked this post is a single bit that has to be exact and durable, because it drives the heart in front of me: if that write is lost, or folded in asynchronously and read back before it lands, the heart fills on the tap and empties on the next scroll past, which reads as a broken app in a way a slightly stale total never does. So membership is a real write keyed by post and user, and it is what makes the event stream safe too — replay a like event twice and the count drifts upward unless the fold deduplicates against that same key. The other consequence is that a split counter is a read cost as well as a write relief: a total spread over N parts is N reads and a sum, so I wouldn’t shard every post by default. Promote a post to a sharded counter once it runs hot, and cache the summed total briefly for everything else.

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.

Why this piece earns its place

A TTL expires one copy, and by now a story exists in three places with three different lifetimes. The row expires on its TTL. The pixels sit in the object store, which reclaims nothing unless a lifecycle rule tells it to. And the image is cached at the edge under the very long TTL we chose for immutable media, so that URL keeps returning a perfectly good photo after the story has vanished from every feed. That gap is fine as “it disappeared from my friends’ view” and not fine as a deletion guarantee, so I want to be careful which one I promise. Story media gets a short-lived link and its own lifecycle rule on the bucket, and expiry is enforced on read — the read path filters by expires-at rather than trusting the store to have already reclaimed the row, because reclamation is lazy and an expired row can stay readable for a while. Put plainly: the TTL is what makes cleanup free, and free cleanup is not the same thing as a delete.

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.

Why this piece earns its place

Hashing by id buys an even spread by giving up locality, and the bill arrives on the read. A feed is a list of post ids from many different authors, so hydrating one screen scatters across most of the shards and the user waits on the slowest of them — the request’s latency stops being the average shard and becomes the worst one in the batch, which is where every slow disk, every compaction and every noisy neighbour now surfaces. So hydration goes out in parallel under a deadline, hot post metadata sits in a cache in front of the shards so the common screen never touches them, and anything needing a cross-shard join or a global ordering gets designed out rather than tuned. The second thing I’d settle before it bites is the bucketing. Taking an id modulo the number of shards means the day you add one, nearly every key moves — a migration of the whole dataset to buy a little headroom. Hash into a large fixed set of logical buckets and map buckets onto machines instead, and growth moves a slice while everything else stays put.

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.

Deep cut · 13:50

One photo, from a kitchen to a queue.

The interactive build above lays out a photo feed step by step. The film follows one photo, Ivy’s loaf of bread, to Jo’s phone in a pharmacy queue: the original saved first with smaller copies behind it, pictures on a shelf beside facts in a cabinet, a follow book read both ways, a post id dropped into every follower’s mailbox, a tally split across helpers, an editor that orders the list, and the giant account that is never pushed and is merged in at read time.

  • See the numbers: 100,000,000 photos a day is over 1,100 a second. 5,000,000,000 opens a day is about 58,000 a second. Building each feed at read time is 300 lookups, 1.5 s, and 17,400,000 lookups a second.
  • Written, not searched: 1,157 posts a second x 200 followers is 231,400 small writes a second, so opening the app is one lookup of 50 ids. A mailbox of 800 ids is 6.4 KB, 3.2 TB for 500,000,000.
  • Take it into the interview: split a hot counter (16,700 taps a second over 20 helpers is about 830 each), and never push an account with 400,000,000 followers: 4,000 s of drops becomes 3 extra lookups at read time.

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…

    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…

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

  3. The many thumbnail sizes are generated…

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

  4. A precomputed feed cache makes scrolling…

    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…

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

Worked example: one photo from a 200-follower account, to one follower’s feed and a like

Ivy posts a photo of a loaf to her 200 followers. Jo, one of them, opens the app a minute later and taps the heart. Site-wide figures come from the film's arithmetic: 100,000,000 photos a day is about 1,157 a second, and 5,000,000,000 opens a day is about 58,000 a second.

StepWhereWhat happensNumbers
1API Gateway → Object StoreThe original bytes are written first, so a crash leaves an orphan object, never a post pointing at nothing.1 original
2Metadata DBThen the row: caption, author, timestamp and the object key. It holds the pointer, not the pixels.1 small row
3Image ProcessingWorkers make the display sizes asynchronously, each under its own derived key. The post is held out of other feeds until they're done.~4 stored sizes
4Events → Feed CacheFan-out on write pushes the post id into each follower's list.200 writes; site-wide 1,157 × 200 = 231,400 a second
5Feed CacheJo opens the app. That's one lookup of her precomputed list. The list is capped at 800 ids.1 lookup, 50 ids; 800 ids = 6.4 KB
6Hybrid mergeJo also follows an account with 400,000,000 followers. Its posts were never pushed, so they're pulled and merged here.+3 lookups, instead of 4,000 s of pushes at 100,000 a second
7Metadata shardsThe 50 ids hydrate in parallel under a deadline. A hot-post cache sits in front, so most of the screen never touches a shard.latency = slowest shard hit
8CDNImage URLs resolve at the edge. They are immutable and hash-named, so there's nothing to invalidate.~95%+ of bytes from edge
9Events → CountersThe tap writes Jo's exact like (post, user), bumps the heart optimistically, and emits an event. A viral post's total is split across helpers.16,700 taps a second ÷ 20 = ~835 each

The trace moves cost from the reader to the writer. Row 5 is cheap only because row 4 paid at post time (Step 4). At 10× posting, fan-out reaches 2,314,000 writes a second and breaks first. That's why Step 4 fans out only to recently active followers, and why Step 7 pulls giant accounts instead of pushing them. On the read side, row 7 is the risk, because a feed scattered over every shard waits on the slowest one. The hot-post cache and parallel hydration in Step 7 handle that. Images (row 8) scale with the CDN, not with us.

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.

The answer, out loud

What a strong answer to “Design Instagram” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–4 min

    What this app actually is

    Let me name the shape of the problem first, because it decides everything after. A few people post photos and an enormous number of people scroll them, so this is read-heavy, and what gets read is mostly bytes rather than rows. A post is a short caption with an author and a timestamp, plus a few megabytes of image, and those two things want completely different storage. So I’ll keep media out of the database, get it as close to the viewer as I can, and make opening the app a lookup instead of a query.

  2. 4–9 min

    Bytes in one place, rows in another

    The skeleton is a gateway in front of two stores. When you post, the image goes into an object store and a small row goes into the metadata database — caption, author, timestamp, and where the object lives. The row points at the pixels; it never contains them. I’d push back hard on an image column, because a multi-megabyte binary in every row makes the database slow to query and miserable to back up, and the database is the thing I most need to stay fast. Blob storage is cheap and durable and does nothing clever, which is exactly what bytes want.

    Built in step 1: Bytes here, rows there
  3. 9–15 min

    Get the pixels near the viewer

    Serving every image out of one central store means every viewer pays the distance to it, and in a photo app a slow image is a slow app. So a CDN goes in front. The first person near an edge pulls the photo through and everyone after them is served locally. Because an uploaded photo never changes there is nothing to invalidate, so the edge can hold it essentially forever — and that is why this fits so well. The hard part of caching is usually knowing when to throw something away, and here we simply don’t have to.

    Built in step 2: Push photos to the edge
  4. 15–20 min

    One upload, several sizes

    A phone rendering a small thumbnail shouldn’t be downloading a twelve-megapixel original, so each photo needs a handful of sizes. I wouldn’t make them during the upload, though. That puts heavy processing directly in front of the user and ties how fast anyone can post to how many encoders I happen to have. So I store the original, respond, and hand the work to a pool of workers that write the variants back a moment later.

    Built in step 3: Process uploads async
  5. 20–27 min

    Make scrolling a lookup

    Now the feed, which is the hottest path in the product. The naive version asks, on every single refresh, for recent posts from everyone you follow and merges them — a large query run by everybody at once, and the first thing to fall over. I’d rather do that merge once, at post time, and push the new post id into the feed lists of the people who follow that account. Then opening the app is reading one list of ids and filling in image URLs, which is cheap and predictable. It costs more on write and the feed runs slightly behind, and for scrolling photos that is an easy trade.

    Built in step 4: Precompute the timeline
  6. 27–33 min

    Counting likes without a hot row

    Likes are where an ordinary design breaks, because a popular post funnels millions of taps a minute onto one row, and incrementing that row synchronously turns the post into a lock everything else queues behind. So a tap writes an event and returns immediately, the interface bumps the number optimistically, and the real total is folded in behind the stream. For the hottest posts the counter itself is split into parts, so taps spread across them and a read adds them back up. The number can trail by a few seconds, and for a like that is completely fine.

    Built in step 5: Counters the async way
  7. 33–38 min

    Things that clean up after themselves

    Stories are the small one, and I like them because they show a principle. They vanish after twenty-four hours, and I don’t want a job scanning the database for old ones — at this size that job is never not running. Instead lifetime becomes a property of the data: each story carries a time-to-live, the store expires it, and feeds just stop finding it. The same idea covers sessions and one-time codes. It replaces a chore with a rule.

    Built in step 6: Stories & ephemerality
  8. 38–43 min

    Scale, and the accounts that break it

    Then scale. One database won’t hold this, so metadata is sharded by id and the stateless services sit behind a load balancer. The interesting part isn’t the median user, it is the extremes. Someone with fifty million followers breaks fan-out on write outright — one post would mean fifty million list writes — so their posts aren’t pushed at all; they get pulled at read time and merged into the feed. The same shape shows up in the counters, where one post is one key. The rule I’d state is that you design for the median and then special-case the outliers, because a social graph always has them.

    Built in step 7: Shard everything
  9. 43–45 min

    Where I’d go next

    So the through-line is that nearly every fast path here is bought with a little staleness — the feed is precomputed, the counts are folded in later, the images are copies sitting at the edge — and the one place I refused to trade was whether your own like registered. With more time I’d take on ranking, which I’d keep at read time over the candidate ids so the model can change without rebuilding everyone’s feed, and moderation, which belongs on the same async path as resizing so it runs before a photo travels far.

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
NextDesign a Rate Limiter Closest system design to this one

Explore the topic

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

More System Designs