Courses / System Design Interview / Chapter 30
Module 3 · Design Real Products · Chapter 30 of 39
Design a Photo-Sharing App
Builds on Back-of-the-Envelope Estimates, Content Delivery Networks, Unique IDs and Sequencers, Message Queues, Blob Stores, Sharded Counters, Design a Video Streaming Platform, Design a Microblog and Design a News Feed. Next up: Design a Group Chat App.
Part 1 · Requirements and Scale
The brief
At 11 pm on the last night of a college fest in Pune, the headline band finishes and 20,000 students post photos of the same stage. Meera's 2.3 MB photo crawls up on one bar of signal and stalls at 80% when the crowd's network chokes. Her 300 friends on the buses home want it in their feeds within seconds, sharp on their screens and light on their data. Somewhere in the same flood, someone uploads an image that must never reach anyone. The app has to keep every photo, scan each one before anyone else sees it, and serve each one hundreds of times without the bill running away.
The product: a photo-sharing app. People upload photos with a caption, follow friends and scroll a feed of new photos. Plan for 100 million daily users and 10 million uploads a day, many from phones on weak networks. Meera's fest photo, ph_19Z, runs through the whole chapter.
What we are building
What exactly must the app do for Meera and her friends? People upload photos with a caption, follow friends and accounts they like, and scroll a feed of new photos. Photos change the problem in three ways compared with the text posts of the microblog chapter:
- Bytes, not words. Each upload is megabytes, not bytes, so moving and storing bytes becomes the main cost.
- Many sizes. A phone in a feed needs a 480-pixel image, not the 12-megapixel original, so a processing step sits between upload and display.
- Hidden harm. An image can carry harm that no word filter catches, so every photo needs a safety check before anyone else sees it.
The scope for today follows from those three:
| In scope today | Out of scope |
|---|---|
| Upload a photo (up to 20 MB) with a caption | Video and stories (the video streaming chapter) |
| See each photo at the right size for the screen | Filters and editing, which happen on the phone |
| Follow and unfollow accounts | Direct messages |
| A home feed of photos from accounts you follow | Search beyond usernames |
| A profile grid of one account’s photos | Ads |
| Like and comment, with counts | Face recognition and tag suggestions |
| Scan every photo, and hold risky ones for review | The tools and policies moderators use |
The 20 MB limit is a design decision, not a detail: it decides whether one upload request is enough or uploads must resume, and phone cameras already produce several megabytes per photo. The feed itself is the microblog's, so the new work is on bytes, processing and safety.
How well it must work
Scope says what the app does; these rules say how well, each one checkable:
- Never lose a photo. Once the app shows "uploaded", the original is stored durably in more than one place.
- Safe before shared. No photo reaches another person until an automatic safety scan has passed it.
- Uploads survive bad networks. Meera's upload, stalled at 80%, should resume, not start again.
- Fast viewing. The first screen of the feed appears within about a second on 4G, images filling in as they load.
- Eventual consistency is fine for feeds, likes and comment counts: a few seconds of delay is acceptable.
To size the design, assume these numbers:
| Assumption | Value |
|---|---|
| Daily active users | 100 M |
| Photos uploaded per day | 10 M (1 in 10 users posts one) |
| Photos viewed per user per day | 100 |
| Original size after phone compression | about 2 MB |
| Peak upload rate | about 10× average, on festival nights and New Year’s Eve |
10 M uploads a day is about 115 per second on average and roughly 1,200 per second at a festival peak. Views are 100 M × 100 = 10 B a day, about 115,000 per second. Reads outnumber writes by about 1,000 to 1, and that ratio shapes everything that follows: the upload path must be safe, the view path must be cheap.
Estimates: storage grows fast
How much disk do 10 million photos a day need? Run the estimate the way the estimates chapter taught: count × size × retention × copies. Each photo is stored once as an original and four more times as resized copies, about 10, 40, 150 and 400 KB for widths of 150, 480, 1080 and 2048 pixels. Nothing is overwritten. For durability, assume erasure coding from the blob store chapter: data pieces plus parity pieces that survive lost disks at about 1.5 times the size, against 3 times for three full copies.
Try it first: 10 M photos a day, each a 2 MB original plus about 0.6 MB of resized copies, stored with erasure coding at 1.5×. How much raw disk do you add each year?
- 10 M × 2.6 MB = 26 TB of new data a day.
- 26 TB × 365 ≈ 9.5 PB a year.
- × 1.5 ≈ 14 PB of raw disk a year; three full copies would need about 28 PB.
Fourteen petabytes a year rules out a database for the bytes. A blob store, from the blob store chapter, is built for exactly this: huge numbers of files that never change, cheap per gigabyte. The other estimates follow the same way:
| Quantity | Working | Result |
|---|---|---|
| Upload bandwidth | 115 uploads/s × 2 MB | ≈ 230 MB/s average, ≈ 2.3 GB/s at peak |
| View bandwidth | 115,000 views/s × about 100 KB, mostly 480 and 1080 px | ≈ 11.5 GB/s, about 90 Gbps |
| Photo metadata | 10 M rows × 500 bytes a day | ≈ 5 GB/day, ≈ 1.8 TB/year |
| Egress per month | 11.5 GB/s × 2.6 M s | ≈ 30 PB; about $1.5 M at $0.05 per GB |
| Storage after one year | 9.5 PB × $0.02 per GB-month | ≈ $190,000 a month, growing every month |
Bytes and facts about bytes have very different shapes: 14 PB of photos a year but under 2 TB of photo rows, so they get different stores. Rented storage is priced per stored gigabyte, with the redundancy included. Prices change, so check current ones, but the shape holds: egress costs several times more than storage, and storage grows every month.
Part 2 · API, Data and the First Design
The API: bytes never touch it
Where should Meera's 2.3 MB go first? Not to the API servers. They hand out permission to upload, and the phone sends the bytes straight to the blob store. The first call also creates the photo's row as an unpublished draft, so the app can upload while Meera is still typing her caption; POST /v1/photos adds the caption and publishes it.
POST /v1/uploads {"content_type": "image/jpeg", "size_bytes": 2310000, "sha256": "9f2c..."}
-> 200 {"upload_id": "up_7Kq", "put_url": "https://blobs.example.com/raw/up_7Kq?sig=...", "expires_in": 900}
PUT {put_url} <photo bytes> (goes to the blob store, not the API)
POST /v1/photos {"upload_id": "up_7Kq", "caption": "Fest night"} -> 202 {"photo_id": "ph_19Z", "status": "processing"}
GET /v1/photos/{photo_id} -> 200 {"status": "ready", "urls": {"thumb": "...", "w1080": "..."}, "like_count": 41}
GET /v1/feed?limit=20&cursor=... -> 200 {"items": [...], "next_cursor": "..."}
GET /v1/users/{user_id}/photos?cursor=...
PUT /v1/photos/{photo_id}/likes/me -> 204 (repeating it is harmless)
POST /v1/photos/{photo_id}/comments {"text": "Front row"} -> 201202 Accepted means "received, still processing": the resized copies appear a few seconds later.
A signed URL is a link that carries its own permission: the object key, an expiry time, a size limit and a signature made with a key the blob store trusts. The blob store checks it alone, with no call back to the API. Meera's allows one upload to one key for 15 minutes.
On a weak network the upload call starts a resumable upload instead: the phone sends the file in parts of, say, 256 KB, and after a dropped connection it asks which parts arrived and sends only the rest. Meera's photo on a 0.5 Mbps uplink takes about 37 seconds. If the signal drops at second 30, a single-request upload starts again from zero; with parts, she loses at most the part in flight, about 4 seconds. The sha256 lets the API recognise a retry of the same photo and return the existing upload.
The phone says the file is a 2.3 MB JPEG. The blob store enforces the size limit, the worker checks that the bytes really are an image, and the safety scan checks what it shows, all before anyone else sees it.
Data model
Once the bytes are in the blob store, everything about them goes to a sharded relational database. Metadata is small, about 500 bytes per photo, but it is read on every feed load.
user_id (pk)handlenameavatar_keycreated_atphoto_id (pk)owner_idcaptionstatus (processing, ready, failed)safety (pending, approved, in review, blocked)published_at (empty for a draft)shared_at (set when fan-out starts)original_keyvariant_keyswidthheightblur_previewcreated_atfollower_id (pk part 1)followee_id (pk part 2)created_atphoto_id (pk part 1)user_id (pk part 2)created_atcomment_id (pk)photo_idauthor_idtextcreated_at- photos owner_id → users
- follows follower_id → users
- follows followee_id → users
- likes photo_id → photos
- comments photo_id → photos
- comments author_id → users
Shard photo rows by owner_id, so Meera's profile grid is one query on one shard. To find a photo by id alone, put the owner's shard number into the id when it is created, in the bits the unique IDs chapter gives to the machine id, and let each shard issue its own ids. The id then says which shard holds the row. In the blob store, each object's key says what it is and how long it lives:
| Object | Blob store key | Kept for |
|---|---|---|
| Raw upload | raw/up_7Kq | 7 days after processing, then deleted |
| Cleaned original | photos/ph_19Z/v1/orig.jpg | Until the user deletes the photo |
| Held for review | review/ph_19Z/v1/... | Until a decision: copied to photos, or deleted |
| Resized copies | photos/ph_19Z/v1/w480.webp, w1080.webp, ... | Until deleted; can be rebuilt from the original |
The raw bucket holds untrusted files and is never served. Only files a worker has checked, cleaned and re-encoded, and the safety scan has approved, reach the photos bucket that the CDN reads from.
High-level design
With the API and the stores in place, three flows share the design. Upload: get a signed URL, send the bytes to the blob store, then check, scan and resize them in the background. View: phones fetch images from the CDN. Feed: app servers build lists of photo ids, as in the microblog and news feed chapters. Here is Meera's photo through the first two:
Watch for the dropped upload: the bytes go to the blob store, never through the API
Read it as steps
The API only signs a URL; the bytes go straight to the blob store, resume after a drop, and friends are served from the CDN.
The API handled two requests of a few hundred bytes each. The 2.3 MB upload, its resumed parts and every view went around it, which is why a modest API tier copes with 115,000 image views a second. The pieces behind the scene:
- Upload API: checks the user, quota, declared type and size, creates a draft row with status
processing, and signs the URL. - Blob store: raw and photos buckets; it emits an event when a raw object lands.
- Resize queue and workers: turn one original into four sizes, with a safety scan on the way.
- Safety service: checks every cleaned photo against known illegal images and with classifiers.
- Metadata database: photo rows, follows, likes and comments, sharded, with a cache in front.
- CDN: caches images near users and pulls from the photos bucket on a miss, as in the CDN chapter.
- Feed: the microblog's fan-out, with big accounts merged at read time. One rule is new: fan-out starts only when a photo is published, ready and approved.
Part 3 · Deep Dives
Processing photos with workers
Every crash, retry and festival burst lands in this pipeline, and a mistake here either loses a photo or shares one twice.
Resizing takes from a few hundred milliseconds to about a second of CPU, so it never runs inside a user's request. The blob store's event puts a job on the resize queue, and a pool of workers drains it. For Meera's photo, one worker does this:
- 1Take a jobit names the upload; while this worker holds it, the job is hidden from others
- 2Download and checkis it really an image, within limits on file size and pixel count
- 3Clean itfix the rotation; remove location and camera data unless the user chose to share them
- 4Scanapprove, hold for review, or block, which ends the job; the next pages cover it
- 5Write the filesoriginal and four sizes to the photos bucket, or the review bucket if held, plus a tiny blurred preview in the row
- 6Mark it readyupdate the row and publish a "photo ready" event
- 7Acknowledgeonly now is the job removed from the queue
Workers crash. If one dies after step 5 but before step 7, the job becomes visible again and another worker repeats it. That is safe because every step is idempotent: output keys are fixed by the photo id (photos/ph_19Z/v1/w480.webp), so a rerun overwrites the same files; setting ready twice changes nothing; a second "photo ready" event cannot start fan-out twice. This is the at-least-once pattern from the message queue chapter. Set the hiding time well above the slowest job, or two workers will process the same photo at once: harmless, but wasted CPU.
One photo can crash every worker it touches. A decompression bomb is a small file whose header claims, say, 50,000 × 50,000 pixels: decoded at 4 bytes a pixel, that is 10 GB of memory. Step 2 reads the dimensions from the header and rejects it before decoding, and a job that fails three times goes to a dead-letter queue instead of crashing the fleet.
Scaling the workers
How many workers does a festival night need? Count CPU-seconds, as the estimates chapter would:
| Average | Festival peak | |
|---|---|---|
| Uploads per second | 115 | 1,200 |
| CPU seconds per photo | 0.5 | 0.5 |
| CPU cores busy | ≈ 60 | ≈ 600 |
| 16-core workers, with 2× headroom | ≈ 8 | ≈ 75 |
The peak needs about ten times the average fleet, for a few hours a year. The queue turns that into a choice: run enough workers for the peak, or fewer and accept a backlog. Twenty 16-core workers process about 640 photos a second. A one-hour peak of 1,200 a second then leaves about 2 M photos waiting, and once uploads fall back to normal, roughly another hour to drain.
So autoscale on the age of the oldest job, not on CPU: age is what a user feels, and once every worker is busy, CPU sits at 100% whether the backlog is one minute or one hour. Meera sees her own photo at once from the local copy on her phone, so a few minutes of delay on festival night costs her friends a short wait, not a lost photo.
Some photos still fall through the cracks: a lost blob store event, a job in the dead-letter queue, a scan that never answered. A sweeper runs every few minutes, re-queues photos still processing after 10 minutes, and after several failed tries over a few hours marks them failed and tells the uploader. Without it, a photo can sit in processing forever with no error anywhere.
Step 4 of every job, the scan, is the one step that decides whether a photo may be shared at all. What does it check?
Content safety: scan before sharing
Any app that lets strangers share images will be used to share illegal and harmful ones. So processing includes a scan, and no photo reaches another person until it has passed. The uploader still sees their own photo at once, from the local copy. The scan works in two stages:
- Match known illegal images. Compute a perceptual hash, a short fingerprint that barely changes when an image is resized or recompressed, and compare it with hash lists of known illegal images supplied by child-safety organisations. A match blocks the photo for good, locks away the evidence and is reported as the law requires.
- Classify the rest. Models score each photo for nudity, violence and other policy categories, and the caption gets a text check. A high score blocks, a middle score holds the photo "in review" for a person, and a low score approves.
The middle band is a trade-off you set. Make it wide and moderators drown in harmless photos; make it narrow and harmful ones slip through. Start stricter for new accounts and accounts with past strikes, and let viewers' reports feed the same review queue, because no scan catches everything.
Hash matching takes milliseconds; classifiers are heavier and may need GPUs, so the safety service runs and scales on its own. If it is down or slow, the design fails closed: jobs return to the queue and retry with backoff, and nothing is shared without a verdict. Skipping the scan to catch up would trade a short delay for images no one checked. The review tools themselves, queues, policies and moderator workflow, are a separate product.
The scan is one of three facts a photo needs before fan-out, and they arrive in no fixed order. Which one should start it?
The share gate
Meera's photo needs three facts before her friends see it: published (she tapped Post), ready (the sizes exist) and approved (the scan passed). Start fan-out on publish alone and friends get grey boxes, or an unscanned image.
Predict before you watch: the caption arrives first, the sizes next, and a moderator's approval an hour later, while one event is delivered twice. Which event starts fan-out, and how often?
- The last new fact, here the approval: friends need published, ready and approved, all three.
- Once: the duplicate finds nothing left to change.
Watch for the photo row: three facts arrive in any order, and only the last shares
Read it as steps
The trigger is one conditional update on the photo's row: set shared_at only where the photo is published, ready and approved and shared_at is still empty. Whichever event completes the set wins that update and starts fan-out; every repeat, and every event that arrives too early, changes nothing.
The update and the fan-out job commit in one transaction, the outbox pattern from the message queue chapter, so a crash between them cannot lose the share. A held photo's files wait in the review bucket, and approval copies them to the photos bucket before the update runs.
Storage tiers and the bill
Meera's photo will be viewed hundreds of times this week and almost never next year. Storage is the bill that only grows, about 9.5 PB a year with nothing deleted unless a user asks, so old bytes need not sit on fast, expensive storage. Three tiers follow the photo's age:
| Tier | What lives there | How it is stored |
|---|---|---|
| Hot, first 30 days | The original and all four sizes | Full copies on fast disks, so CDN misses are quick |
| Warm, after 30 days | The four sizes, and originals until day 90 | Erasure-coded at about 1.5×; a slower read is hidden by the CDN |
| Cold, after 90 days | Originals, rarely read once the sizes exist | An archive tier 20 to 50 times cheaper; a read takes minutes to hours |
The originals are the prize: 2 MB of every 2.6 MB. In the archive at a twentieth of the price, an old photo costs as much as 0.6 MB + 2 MB ÷ 20 = 0.7 MB at full price, about a quarter of before. Once most photos are older than 90 days, that cuts the storage bill by about two thirds. Lifecycle rules from the blob store chapter move objects by age, so no job walks billions of files. The catch: an archived original takes hours to restore, so adopting a new image format, which re-reads every original, becomes a planned batch job.
Egress is still the larger line, about 30 PB a month. The CDN sends every byte a user sees, so the levers are smaller files: modern formats such as WebP or AVIF, often 25 to 50% smaller, and the right size for each screen. A high hit ratio keeps origin traffic small, and versioned URLs make long cache lifetimes safe:
def photo_urls(photo): base = f"https://img.example.com/photos/{photo.id}/v{photo.version}" return {size: f"{base}/{size}.webp" for size in ("thumb", "w480", "w1080", "w2048")}
The version in the path lets you replace a file without waiting for old copies to expire from CDN caches.
Part 4 · Failures and Trade-offs
Failures and edge cases
An upload touches the phone, the API, the blob store, a queue, workers and the scanner. Each can fail on its own, so every step must be safe to retry. Here is what each failure looks like and how the design copes:
| What goes wrong | What happens | How the design copes |
|---|---|---|
| The network drops mid-upload | Part of the file arrived | Resumable upload: ask which parts arrived, send the rest |
| A draft never finishes | Stuck in processing, or never published | The sweeper retries, then fails it; unpublished drafts and files are deleted after 7 days |
| A signed URL leaks | Someone else could upload | It expires in 15 minutes and works for one key and one size limit |
| The file is not really an image | A possible attack | The worker rejects it, marks the photo failed and deletes the raw file |
| The safety service is down | New photos stay unshared; uploaders still see their own | Fail closed: jobs retry with backoff; never skip the scan |
| A zone or a region fails | A zone: nothing visible. A region: its uploads fail over | Blob data spans zones; the app uploads to another region, metadata replicas take over, originals are copied across regions, and the CDN keeps serving |
| A user deletes a photo | It must vanish everywhere | The row is marked deleted at once; feeds drop it; a background job removes files and CDN copies |
Almost every row is handled by a retry that is safe to repeat, or by a rule that fails closed. Deletion needs one more step: images are cached for months, so the delete sends a purge request for the photo's URLs, and private photos use signed CDN URLs that stop working within minutes.
Finally, durability is not backup. Replication and erasure coding protect against failed disks, but a buggy cleanup job deletes every replica too. Keep deleted originals for a short grace period, unreachable by anyone, and make bulk deletions go through a job that can be paused and rolled back.
Trade-offs
Most choices in this design have a reasonable alternative, and each pick holds only under a condition:
| Decision | Option A | Option B | Our pick |
|---|---|---|---|
| Upload path | Through the API servers | Signed URL straight to the blob store | B: the API tier stays small |
| Resized copies | Made right after upload | Made on first request by an image service | A for the 4 common sizes; B for rare ones |
| Durability | 3 full copies | Erasure coding, about 1.5× | Both: replicated while hot, erasure-coded once cool |
| Feed | Fan-out on write | Fan-out on read | Write, merging huge accounts at read time |
| Image format | JPEG only | WebP or AVIF, JPEG as fallback | Modern formats, often 25 to 50% smaller |
Option AThrough the API servers
Option BSigned URL straight to the blob store
Our pickB: the API tier stays small
Option AMade right after upload
Option BMade on first request by an image service
Our pickA for the 4 common sizes; B for rare ones
Option A3 full copies
Option BErasure coding, about 1.5×
Our pickBoth: replicated while hot, erasure-coded once cool
Option AFan-out on write
Option BFan-out on read
Our pickWrite, merging huge accounts at read time
Option AJPEG only
Option BWebP or AVIF, JPEG as fallback
Our pickModern formats, often 25 to 50% smaller
Each pick spends a little complexity to save bytes or servers. Resizing on the fly shows the condition: an image service in front of the blob store makes any size on request and lets the CDN cache it, saving storage for sizes nobody asks for, but the first request for each size pays the CPU cost while a user waits. If the app needed dozens of sizes, on demand would win.
Phone photos often carry GPS coordinates inside the file. Serving them unchanged can reveal where someone lives, so the worker removes them by default.
CDN egress dominates the bill, about 30 PB a month; storage comes second and grows every month.
Part 5 · Going further
Likes, comments and hot photos
Optional: what happens when a photo goes viral. Likes are tiny writes, but they cluster: a film star's photo can collect tens of thousands of likes in its first minute, all aimed at one counter. Four rules keep that manageable:
- Store each like as a row keyed by (photo_id, user_id). Liking twice hits the same key, so only a new row adds to the count, and "did I like this?" is one lookup.
- Count with sharded counters, as in the sharded counters chapter: each like adds 1 to one of, say, 16 shard counters, and the displayed total is their sum, cached for a few seconds.
- Show rounded counts for big numbers. "1.2 M likes" need not be exact to the last like.
- Page comments by photo id and time with a cursor, loading the first few with the photo.
Hot photos are mostly a read problem, and the CDN absorbs it: the same 1080-pixel file is served from edges millions of times while the blob store sees a handful of requests. The hit ratio decides how much reaches the origin. At 115,000 views a second, a 95% hit ratio sends 5,750 requests a second to the blob store, about 575 MB/s; 99% cuts that to 1,150, five times less. That is why versioned URLs and long cache lifetimes matter so much. Each request type has its own fast path:
| Request | Served by | Typical latency |
|---|---|---|
| Image bytes | CDN edge (on a miss, the blob store) | 20 to 50 ms (a miss: a few hundred) |
| Photo metadata | Metadata cache (on a miss, a database shard) | about 1 ms (a miss: about 10 ms) |
| Like count | Cached sum of shard counters | about 1 ms |
| Feed list | Feed cache | about 2 ms |
Every hot read is served from memory or an edge; only misses touch a disk.
Part 6 · Interview kit
The 45-minute answer
Here is the whole answer on the interview clock: about 40 minutes of design once introductions are done.
| Minutes | What you say and draw | Key number or decision |
|---|---|---|
| 0–5 | Upload with caption, right size per screen, follow, feed, likes. Uploads resume; photos are never lost; nothing is shared before a safety scan. Out: video, DMs, search, review tools. | Views beat uploads about 1,000 to 1 |
| 5–8 | Uploads per second, storage per year, view bandwidth. | 115 uploads/s, 1,200 at peak; about 14 PB a year; about 90 Gbps |
| 8–13 | POST /uploads returns a signed URL and a draft; PUT the bytes to the blob store; POST /photos publishes. Rows sharded by owner_id. | The API never touches photo bytes |
| 13–22 | Upload API, blob store with raw and photos buckets, resize queue and workers, safety service, metadata database, CDN, the microblog’s feed. Walk one upload and one view. | Fan-out waits for published, ready and approved |
| 22–37 | The pipeline: idempotent steps, poison photos, the sweeper, autoscaling. Then the scan and share gate, or storage tiers if cost comes up. | 0.5 CPU-seconds a photo; about 75 workers at peak |
| 37–40 | Worker crash, region loss, scanner down; what you monitor; what dominates the bill. | CDN egress, about 30 PB a month |
Pick the deep dive from the interviewer's questions. Uploads, weak networks or crashes: the processing pipeline. Abuse, safety or legal risk: the scan and the share gate. Cost or petabytes: storage tiers and egress. If they ask about the feed, say it is the microblog's design with one gate, and move on.
- Say
- "The API signs a URL and the phone sends the 2 MB straight to the blob store, so photo bytes never touch the API tier."
- Say
- "Resizing takes about 0.5 CPU-seconds a photo, so it runs on idempotent workers behind a queue, never inside the request."
- Say
- "Egress is the biggest bill, about 30 PB a month, so every byte counts: modern formats, the right size per screen, long-cached versioned URLs."
- Avoid
- "We store the photos in the database." At 14 PB a year it is the wrong tool.
- Avoid
- "We fan out when the upload starts." Followers would see grey boxes, and unscanned photos would reach feeds.
Interview drill
Interviewers often change the problem halfway to see whether the design bends or breaks. Answer each one before opening it.
A film star posts a wedding photo: 20 M views and 2 M likes in the first hour. What gets hot, and how does each part cope?
- Image bytes: each CDN edge misses once, and an origin shield in front of the blob store turns those misses into a few fetches.
- The metadata row: cached on several replicas and in each server's memory for a few seconds.
- Likes: 2 M an hour averages about 550 a second, far more in the first minutes; sharded counters spread the writes.
- Fan-out: the star is above the pull threshold, so followers' feeds merge the photo at read time.
Product adds albums: one post with up to 10 photos and a single caption. What changes?
- Split posts from photos: a posts row holds the caption and publish time; photo rows point to it with a position.
- Each photo is processed and scanned on its own, in parallel; the post is ready when every photo is ready and approved.
- Fan-out, likes and comments attach to the post; decide whether a blocked photo drops out or holds the whole post.
A new law says photos of users in India must be stored and processed in India. Their followers are worldwide. What changes?
- Give each user a home region: their buckets, workers, safety scan and metadata shard live there.
- Ask whether CDN copies abroad are allowed; if not, serve these photos from Indian edges only, slower for distant followers.
- Feeds abroad hold only photo ids; hydration reads rows from the home region, up to about 200 ms away.
- Common miss
- Sending photo bytes through the API servers. At 2.3 GB/s of uploads at peak, a tier built for small requests becomes a bandwidth problem.
- Common miss
- No safety step. "We would add moderation later" means unscanned images reach feeds from the first day.
Wrap-up
Next, Design a Group Chat App moves from files to conversations: persistent connections, ordering within each chat and delivery to people who are offline.
Part 7 · Check yourself
Quick check
Q1Followers sometimes see a grey box where a new photo should be, for a few seconds after it is posted. What is the likely cause, and the fix?
Think it through, then check.
- Fan-out starts before the resized copies exist, probably on the publish call alone, so apps ask the CDN for a w1080 file not yet written.
- Fix: start fan-out only when published, ready and approved are all true, with one conditional update on the photo row.
Q2Product wants 30-second videos alongside photos. What changes in processing, storage and the bill?
Think it through, then check.
- Processing becomes transcoding into segments at several bitrates (the video streaming chapter): tens of CPU-seconds per clip, not half a second.
- Its five versions at 9.7 Mbps in total are about 36 MB per clip, against 2.6 MB per photo; if a tenth of uploads are videos, yearly storage more than doubles.
- Each view sends 50 to 100 times a photo's bytes, so start clips muted, at a low bitrate, once in view; the scan must sample frames and audio.
Q3Should the safety scan block sharing until it answers, or should photos go out at once and be removed if flagged later?
Think it through, then check.
- Blocking adds about a second on top of resizing, which nobody notices; the uploader already sees a local copy.
- Sharing first lets a harmful image reach thousands before it is pulled, and a known illegal one is a legal problem the moment it is served.
- So block on hash matching for everyone; trusted accounts' photos may go out after it while classifiers finish, a named risk.
Q4The safety service is down for 20 minutes on a festival night. What do uploaders and followers see, and what happens when it recovers?
Think it through, then check.
- Uploaders see their own photos; followers see nothing new, because the design fails closed.
- At 1,200 uploads a second, about 1.4 M photos wait, retrying with backoff; on recovery workers scale on queue age, and each approval starts fan-out once.
- Stop the sweeper from failing photos that waited only because of the outage.
Q5Which building block serves 115,000 image views a second, and what decides how much traffic still reaches the blob store?
Think it through, then check.
- A CDN, from the CDN chapter: edges near viewers serve the bytes, so the blob store sees only misses.
- The hit ratio decides the rest, and versioned URLs let files stay cached for months safely, which keeps it high.