Design Photo Sharing

Design a photo app with follows and a feed: direct uploads, a resizing pipeline, CDN delivery, the metadata partition key and hot celebrity posts.

Case study30 minLesson 1 of 10
On this page
  1. Clarify the problem
  2. Estimate the scale
  3. Define the API
  4. Model the data
  5. High-level design
  6. Deep dives
  7. Bottlenecks and failures
  8. What a strong answer sounds like
  9. Follow-up questions

You take a photo, add a caption and tap Share. Seconds later your friends see it in their feed, sharp on a big phone and quick on a slow network. Instagram is the kind of product this question means: upload photos, follow people, scroll a feed.

Interviewers like it because it mixes two kinds of data. Photos are huge, never change and are read millions of times, so they belong in object storage behind a CDN. Metadata (posts, follows, likes) is tiny, changes constantly and must be partitioned well. A good answer keeps the two apart. The steps follow A Framework for Any Design. The feed itself is covered in Design a News Feed, so this lesson spends its words on what photos change.

Clarify the problem

The first five minutes
Interviewer

Design a photo sharing app.

You

Users upload a photo with a caption, follow people, see a feed of followed accounts, view a profile grid, and like posts. Are comments, stories, video or messages in scope?

Interviewer

Leave out stories, video and messages. Likes yes.

You

What scale, and how skewed are follower counts?

Interviewer

200 million daily users, about 50 million new photos a day. Most people have a few hundred followers; some accounts have 50 million.

You

Then the bytes are almost all images. My priorities: an uploaded photo is never lost, images load fast anywhere, and the feed is quick even if a few seconds stale. A post reaching followers within 10 seconds is fine, and like counts can be approximate.

  • Functional: upload a photo with a caption; follow and unfollow; home feed, newest first; profile grid; like and unlike.
  • Non-functional: no photo lost once the app says "posted"; feed served in under 200 ms; images start showing within about a second; posts reach followers within 10 seconds; reads stay available, a few seconds stale at worst.
  • Out of scope: video, stories, messages, search, ads, ranking models.

Estimate the scale

A day has 86,400 seconds. Assume peaks are 3 times the average.

  • Uploads: 50 million ÷ 86,400 ≈ 580 photos a second, about 1,700 at peak.
  • Feed loads: 10 opens per user per day is 2 billion ÷ 86,400 ≈ 23,000 a second, about 69,000 at peak.
  • Bytes per photo: a 2 MB original plus three sizes we make: 30 KB thumbnail, 120 KB feed size, 250 KB full screen. 2 MB + 400 KB = 2.4 MB.
  • Ingest: 580 × 2 MB ≈ 1.2 GB/s into storage.
  • Metadata: about 500 B per post: 50 million × 500 B = 25 GB a day, about 9 TB a year.
Estimate: Photo storage per year

How much photo storage does one year of uploads add, before replication?

You know
  • 50 million new photos a day
  • 2 MB original plus 400 KB of resized copies per photo
  • Nothing is deleted

Show the working
  1. Per day: 50 × 106 × 2.4 MB = 120 TB
  2. Per year: 120 TB × 365 ≈ 44 PB
  3. Originals are 2 ÷ 2.4 of that, about 36.5 PB; the sizes people view are about 7.3 PB

Estimate 44 PB (anything from 15 PB to 150 PB is a good answer)

Originals are rarely read after processing, so moving them to a colder storage class after a month cuts the cost of most of the bytes.

Estimate: Image bandwidth to viewers

Each feed load shows about 10 photos at the 120 KB feed size. What is the average outgoing image bandwidth?

You know
  • 23,000 feed loads a second
  • 10 photos per load
  • 120 KB per photo

Show the working
  1. Per load: 10 × 120 KB = 1.2 MB
  2. 23,000 × 1.2 MB ≈ 28 GB/s, about 83 GB/s at peak

Estimate 28 GB/s (anything from 9 GB/s to 90 GB/s is a good answer)

That is a CDN's job. If the CDN serves 95% of it, the photo store still sends 5% × 28 GB/s ≈ 1.4 GB/s.

The feed JSON for those 10 posts is about 10 KB, so 1.2 ÷ 1.21 ≈ 99% of what a user downloads is pictures.

Your turn: Define the API

Write the endpoints for uploading and posting a photo, reading the feed, following and liking. Where do the image bytes travel?

Compare with a model answer

Ask the API for an upload slot and get a short-lived signed URL, send the bytes straight to object storage, then create the post. Feed reads return metadata with CDN URLs for each size; image bytes never pass through the API.

Define the API

HTTP API
POST /v1/uploads                  body: { "content_type": "image/jpeg", "bytes": 2048000 }
  201 { "photo_id": "p7001", "upload_url": "https://store.../p7001/original?sig=...",
        "expires_in": 900 }
  413 Payload Too Large   over the 20 MB limit
 
PUT  {upload_url}                 the raw bytes, straight to the photo store
 
POST /v1/posts    Idempotency-Key: (client-generated)
  body: { "photo_id": "p7001", "caption": "Harbour at dusk" }
  202 { "post_id": "1849...", "status": "processing" }
  409 Conflict      photo already used by another post
 
GET  /v1/feed?cursor=...&limit=10
  200 { "posts": [ { "post_id", "author", "caption", "likes": 12400,
        "images": { "thumb": "https://cdn.../p7001/t.v1.jpg", "feed": "...", "full": "..." } } ],
        "next_cursor": "..." }
 
GET  /v1/users/{id}/posts?cursor=...      profile grid, newest first
PUT  /v1/users/{id}/followers/me          follow (DELETE to unfollow)
PUT  /v1/posts/{id}/likes/me              like (DELETE to unlike)

The decision worth arguing: creating a post returns 202 with status processing. The bytes reached storage without the API seeing them, so nothing is promised until a worker has checked the file and made the sizes. The author's app shows the post from its local copy; followers see it once it is ready. Follow and like are PUT on me, so a retry is harmless: liking twice is still one like.

Your turn: Model the data

Sketch the records for posts, follows and likes. Pick a partition key for each and say which reads it serves.

Compare with a model answer

Posts partitioned by author and sorted by a time-ordered id, so a profile is one partition read; feed entries store (author, post) pairs. Follows stored once per direction. Likes partitioned by the user who liked, with a separate count per post. A wide-column store fits.

Model the data

Metadata tables (wide-column store)
posts               PRIMARY KEY ((author_id), post_id)   -- post_id DESC
  author_id   BIGINT
  post_id     BIGINT      -- 64-bit, time-ordered
  photo_id    VARCHAR(16)
  caption     TEXT        -- up to 2,200 chars
  status      VARCHAR(10) -- processing | ready | failed | deleted
  width, height, image_version INT
 
followers           PRIMARY KEY ((followee_id, bucket), follower_id)
following           PRIMARY KEY ((follower_id), followee_id)
 
photos              PRIMARY KEY (photo_id)
  post_id     BIGINT NULL, processed BOOLEAN           -- see "upload"
 
likes               PRIMARY KEY ((user_id), post_id)     -- did I like it?
like_counts         PRIMARY KEY (post_id)
  count BIGINT,  last_offset BIGINT                     -- see "hot posts"

Every access is "one key, newest first": an author's posts, a user's followers, a batch of posts for a feed page. Nothing joins. That fits a wide-column store such as Cassandra: it partitions by key, keeps rows sorted inside a partition and takes a high write rate. A relational database would also work for a while, then need sharding by hand.

Post ids come from a time-ordered ID generator, so sorting by id is sorting by time and a cursor is "older than this id". Keep 3 replicas and read and write with a quorum of 2: since 2 + 2 > 3, every read overlaps a replica that has the latest acknowledged write.

Your turn: High-level design

Sketch the boxes: where do bytes go on upload, what makes the three sizes, where are images served from, and when does a post reach followers?

Compare with a model answer

The phone sends bytes to a photo store with a signed URL. The store emits an event; resize workers make the sizes, mark the post ready and trigger fan-out. A CDN serves images; the API serves metadata and asks the feed service for post ids.

High-level design

Two paths that barely touch: a media path moving gigabytes a second, and a metadata path moving small JSON. The "ready" step joins them.

Bytes go straight from the phone to the photo store and out through the CDN. The API only handles metadata; a post reaches followers when the resize workers mark it ready.Phone app to CDN (GET image); Phone app to Photo store (PUT, signed URL); Phone app to API servers (post, feed, follow); CDN to Photo store (on miss); Photo store to Upload events (object created), asynchronous; Upload events to Resize workers, asynchronous; Resize workers to Photo store (3 sizes); Resize workers to Metadata store (mark ready); Resize workers to Feed service (fan out), asynchronous; API servers to Metadata store (posts, likes); API servers to Feed service (feed ids).Phone appCDNAPI serversPhoto storeFeed serviceUpload eventsResize workersMetadata storeGET imagePUT, signed URLpost, feed, followon missobject created3 sizesmark readyfan outposts, likesfeed ids

request and replyasynchronous message

Bytes go straight from the phone to the photo store and out through the CDN. The API only handles metadata; a post reaches followers when the resize workers mark it ready.
Upload, process, view, and two failuresUse the arrows or ← → keys
  1. Upload, process, view, and two failures, step 1Phone app to CDN; Phone app to Photo store; Phone app to API servers; CDN to Photo store; Photo store to Upload events, asynchronous; Upload events to Resize workers, asynchronous; Resize workers to Photo store; Resize workers to Metadata store; Resize workers to Feed service, asynchronous; API servers to Metadata store; API servers to Feed service.Phone appCDNAPI serversp7001Photo storeFeed serviceUpload eventsResize workersMetadata store

    1Mia picks a 2 MB photo. The API reserves p7001 and returns a URL signed for one PUT of at most 20 MB, valid 15 minutes.

  2. Upload, process, view, and two failures, step 2Phone app to CDN; Phone app to Photo store; Phone app to API servers; CDN to Photo store; Photo store to Upload events, asynchronous; Upload events to Resize workers, asynchronous; Resize workers to Photo store; Resize workers to Metadata store; Resize workers to Feed service, asynchronous; API servers to Metadata store; API servers to Feed service.Phone appCDNAPI serversPhoto storep7001 originalFeed serviceUpload eventsResize workersMetadata store

    2The app sends the bytes straight to the photo store, so 1.2 GB/s of uploads costs the API fleet nothing.

  3. Upload, process, view, and two failures, step 3Phone app to CDN; Phone app to Photo store; Phone app to API servers; CDN to Photo store; Photo store to Upload events, asynchronous; Upload events to Resize workers, asynchronous; Resize workers to Photo store; Resize workers to Metadata store; Resize workers to Feed service, asynchronous; API servers to Metadata store; API servers to Feed service.Phone appCDNAPI serversPhoto storeFeed serviceUpload eventsResize workersMetadata storeprocessing

    3The app creates the post. It is stored as processing, the API answers 202, and Mia's app shows the post from its local copy.

  4. Upload, process, view, and two failures, step 4Phone app to CDN; Phone app to Photo store; Phone app to API servers; CDN to Photo store; Photo store to Upload events, asynchronous; Upload events to Resize workers, asynchronous; Resize workers to Photo store; Resize workers to Metadata store; Resize workers to Feed service, asynchronous; API servers to Metadata store; API servers to Feed service.Phone appCDNAPI serversPhoto storeFeed serviceUpload eventsResize workersMetadata store
    attempt
    1

    4The store publishes "object created". A resize worker decodes the file, which proves it is an image, and strips the camera's location data.

  5. Upload, process, view, and two failures, step 5Phone app to CDN; Phone app to Photo store; Phone app to API servers; CDN to Photo store; Photo store to Upload events, asynchronous; Upload events to Resize workers, asynchronous; Resize workers to Photo store; Resize workers to Metadata store; Resize workers to Feed service, asynchronous; API servers to Metadata store; API servers to Feed service.Phone appCDNAPI serversPhoto storeFeed serviceUpload eventsredeliverResize workersdownMetadata store
    attempt
    2

    5The worker crashes halfway. It never acknowledged the event, so after a 60-second visibility timeout the queue gives it to another worker.

  6. Upload, process, view, and two failures, step 6Phone app to CDN; Phone app to Photo store; Phone app to API servers; CDN to Photo store; Photo store to Upload events, asynchronous; Upload events to Resize workers, asynchronous; Resize workers to Photo store; Resize workers to Metadata store; Resize workers to Feed service, asynchronous; API servers to Metadata store; API servers to Feed service.Phone appCDNAPI serversPhoto storeFeed serviceUpload eventsResize workersMetadata storeready
    attempt
    2

    6The second worker writes 3 sizes to keys fixed by photo id and size, overwriting any half-written file, and sets processed on the photo row. The post has already claimed the photo, so the worker marks it ready.

  7. Upload, process, view, and two failures, step 7Phone app to CDN; Phone app to Photo store; Phone app to API servers; CDN to Photo store; Photo store to Upload events, asynchronous; Upload events to Resize workers, asynchronous; Resize workers to Photo store; Resize workers to Metadata store; Resize workers to Feed service, asynchronous; API servers to Metadata store; API servers to Feed service.Phone appCDNAPI serversPhoto storeFeed service400 insertsUpload eventsResize workersMetadata store

    7Only now does fan-out start. Mia has 400 followers, so (Mia, post id) goes into 400 cached feeds. No follower sees a post without its picture.

  8. Upload, process, view, and two failures, step 8Phone app to CDN; Phone app to Photo store; Phone app to API servers; CDN to Photo store; Photo store to Upload events, asynchronous; Upload events to Resize workers, asynchronous; Resize workers to Photo store; Resize workers to Metadata store; Resize workers to Feed service, asynchronous; API servers to Metadata store; API servers to Feed service.Phone appCDNAPI serversPhoto storeFeed serviceUpload eventsResize workersMetadata store

    8Leo opens his feed. The API gets 10 (author, post) pairs from the feed service and reads each post from its author's partition: about 10 KB of JSON.

  9. Upload, process, view, and two failures, step 9Phone app to CDN; Phone app to Photo store; Phone app to API servers; CDN to Photo store; Photo store to Upload events, asynchronous; Upload events to Resize workers, asynchronous; Resize workers to Photo store; Resize workers to Metadata store; Resize workers to Feed service, asynchronous; API servers to Metadata store; API servers to Feed service.Phone appCDNmiss, cachedAPI serversPhoto storeFeed serviceUpload eventsResize workersMetadata store

    9His app fetches each 120 KB feed image from the CDN. The first viewer near this edge misses, so the edge fetches it from the store once and keeps it.

  10. Upload, process, view, and two failures, step 10Phone app to CDN; Phone app to Photo store; Phone app to API servers; CDN to Photo store; Photo store to Upload events, asynchronous; Upload events to Resize workers, asynchronous; Resize workers to Photo store; Resize workers to Metadata store; Resize workers to Feed service, asynchronous; API servers to Metadata store; API servers to Feed service.Phone appgave upCDNAPI serversPhoto storeFeed serviceUpload eventsResize workersMetadata storeswept

    10Another user gets an upload URL and never sends the file. A sweeper deletes posts still processing after 24 hours; a store rule deletes originals with no post.

1 / 10

Deep dives

The media path: upload, resize, serve

Upload. Signed URLs are explained in Blob Storage. What photos add is distrust: the bytes never passed your code. So the signed URL fixes the key, maximum size and content type, and the post stays processing until a worker has decoded the file. A file that does not decode is deleted and the post marked failed.

Ordering. The worker can finish before the app creates the post, find nothing to mark, and leave the post stuck in processing. So both sides write the photo's own row with a compare-and-set: the post request sets post_id only if it is empty (this is also the 409), and the worker sets processed. Whichever write lands second sees both fields, marks the post ready and starts fan-out, exactly once.

Resize. Say 0.2 seconds of CPU per photo for all three sizes: 580 × 0.2 ≈ 120 cores on average, 1,700 × 0.2 ≈ 350 at peak. Autoscale on queue depth, not CPU, because depth is what delays posts.

When to make the resized copies
OptionGood atBad atPick it when
Eagerly, after upload Usual defaultEvery size exists before anyone views the post; serving is a plain file read.A few seconds before the post is ready. A new size later means a backfill over every photo.A small fixed set of sizes. The usual answer here.
On demand at the edgeAny size or format on request; nothing made for photos nobody views.The first view of each size waits for a resize, and a viral post's first seconds send a burst of resize work to the origin.Many devices need many sizes and most photos are rarely viewed.
On the phoneNo server CPU; smaller uploads.You cannot trust or control the output, and you still need a server path for new formats.As a first step to shrink uploads, never the only one.

I would resize eagerly and accept the backfill cost: one year of photos is 18 billion × 0.2 s = 3.6 billion CPU-seconds, about 4,000 cores for 10 days, so choose the size list with care.

Serve. Each image URL holds the photo id, size and a version: /p7001/t.v1.jpg. A URL's content never changes, so the CDN may cache it for a year; re-processing writes v2 and updates the post, and nothing needs purging. Add a regional origin shield between edges and store, so hundreds of edges missing on the same new photo send one request to the store. Private accounts get short-lived signed CDN URLs (see CDNs).

Metadata and the partition key

Posts are read two ways: a profile grid (one author, newest first) and a feed page (10 posts by different authors). One key must serve both.

Partition key for posts
OptionGood atBad atPick it when
By post idLoad spreads evenly; a feed page is 10 direct lookups.A profile grid needs a second table, author to post ids, kept in step.Posts are fetched one at a time and profiles are rare.
By author, sorted by post id Usual defaultA profile is one sorted partition read. Feed entries carry the author, so a feed page is still 10 direct lookups.A famous author's partition gets heavy reads; feed entries are 16 bytes instead of 8.Profiles are a main screen, as here.
By dayEasy to archive old data.All 580 writes a second and all fresh reads land on today's partition.Almost never as the main key for user data.

Partition by author. A heavy poster's partition is 10,000 posts × 500 B = 5 MB, which is small; the hot-read risk is a caching problem, covered next. Follows are written twice, because "who follows Mia" (fan-out) and "whom does Leo follow" (pulling big accounts) live in different partitions. A 50-million-follower list would be 50 × 106 × 16 B = 800 MB in one partition, too big to repair easily, so followers adds a bucket of 1 million followers each: 50 partitions for the biggest account, 1 for everyone else.

Hot celebrity posts

An account with 50 million followers posts. Four things heat up at once.

  • Fan-out. 50 million feed inserts at 200,000 a second take 250 seconds. Use the hybrid from the news feed design: above 100,000 followers, followers pull that author's posts at read time.
  • The post record. If 20% of followers open it within an hour, that is 10 million ÷ 3,600 ≈ 2,800 reads a second of one key, more in the first minutes. The caption and image list never change, so cache the post and keep a copy for a few seconds in each API server's memory.
  • The image. The CDN takes the reads; the origin shield stops the first burst of misses.
  • Likes. If 5% of followers like it in 10 minutes, that is 2.5 million ÷ 600 ≈ 4,200 likes a second on one post.

Likes need the most care. The likes table is partitioned by the user who liked, so those 4,200 writes spread over every partition, and it answers "did I like this?", which every feed render asks. The count is separate: each like also goes to a stream partitioned by post id, and the aggregator that owns a stream partition sums likes per post for one second, then writes count += n once. 4,200 updates a second become 1.

The stream delivers at least once, so after a crash the aggregator may see events again. It stores the last stream offset it applied in the same row as the count, in the same write, and on a replay skips every event at or below it, so no like counts twice. This works only because one aggregator owns all of a post's events, which arrive in offset order. A nightly recount from likes repairs any other drift. The shown count lags about a second and is rounded ("12.4K"), as the requirements allow.

The feed: what photos change

Reuse the hybrid fan-out, cursors and caches from the news feed design. Three things differ: fan-out starts when the post is ready, not when it is created; feed entries hold (author, post) pairs to fit the partition key; and since 99% of the bytes are images, the app picks the size for its screen and prefetches the next page's images, which does more for perceived speed than a faster feed query.

Bottlenecks and failures

At 10x (500 million photos a day) the pain is bytes and money. Storage grows about 440 PB a year, so originals move to a cold storage class after 30 days, with erasure coding instead of full copies. Images average 280 GB/s, so CDN cost becomes the main budget line and a second CDN provider is worth having. The resize fleet grows to 1,200 to 3,500 cores. Metadata and feeds scale by adding partitions.

When this diesWhat happensWhat you do
An API serverRequests in flight fail.The load balancer drops it; post creation is idempotent, so the app retries.
The upload event queueUploads succeed but posts stay processing.A job re-enqueues posts still processing after 10 minutes. No photo is lost.
Resize workersBacklog grows; posts appear later.Autoscale on queue depth; redelivery and fixed output keys make retries safe.
A metadata replicaSome partitions lose one of three copies.Quorum reads and writes (2 of 3) continue; the replica is rebuilt.
The feed serviceCached feeds are gone.Pull recent posts of followed authors: slower, but working.
The CDNImages stop loading; the store cannot serve 28 GB/s.Switch the image host name to a second CDN provider.

What a strong answer sounds like

LevelWhat the answer does
JuniorPuts images in object storage and metadata in a database, adds a CDN, and gets a basic feed working with hints.
MidEstimates storage and bandwidth, uploads with signed URLs, resizes with a queue of workers, and picks push or pull for the feed with a reason.
SeniorSeparates media from metadata with numbers, holds posts until processed, derives partition keys from access patterns, handles the celebrity post on every axis, and plans cold storage for originals.
The partition keyStrong answer
Interviewer

Why partition posts by author? Doesn't that create hot spots?

You

Profiles read one author's posts newest first, so that's one sorted partition read, and feeds still fetch posts directly because each feed entry carries the author. The hot spot is reads of a famous author's posts. Those are immutable, so a cache plus a few seconds of in-process copies absorbs them. Likes are different: they're keyed by the user who liked, and the count is batched.

Follow-up questions

How would you support several photos in one post?

The post holds an ordered list of photo ids and becomes ready only when all are processed. Each photo is uploaded and resized on its own, so ten photos are ten parallel jobs.

A user deletes a photo. What has to happen?

Mark the post deleted so the API and feeds skip it, then delete the objects and purge the CDN in the background. On private accounts, short-lived signed URLs mean copied links die even before the purge.

The phone retries a post after a timeout. Do you get two posts?

No. Post creation carries an idempotency key, so the retry returns the first post, and a photo id can belong to only one post, which the 409 enforces.

How do you make images load fast on a slow network?

Keep a tiny blurred preview, a few hundred bytes, in the post metadata so the feed paints at once. Then load the size that fits the screen, in a modern format if the phone supports it.

How would you add content moderation?

Add a second consumer of the upload events that scores each image. Flagged posts stay out of feeds until reviewed, which is easy because posts already wait for a "ready" state.

Self-review: photo sharing0 of 14

After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".

Before drawing

Numbers and interfaces

Design and depth

Failure