Design a Photo-Sharing App
Designing a photo-sharing app means picking the parts that store, move and serve photos at a stated scale. This capstone builds one complete design with the beginner toolkit, from the interview prompt to the honest trade-offs.
The ask
The interview prompt is short. Design a photo-sharing app. The interviewer adds no scale, no feature list and no starting design.
The app must let a person create an account. It must let a person upload a photo. It must let a person view a photo that someone shared. It must let a person open a profile and browse the photos on it.
Every missing detail is a question to ask before designing. The next step settles those details out loud.
Clarify the requirements
The functional requirements name what the app does. This design covers accounts, photo uploads and photo viewing. Uploading a photo and viewing a photo are the two flows that matter.
The non-functional requirements name how well the app must work. Views should load quickly for people far away. The app should stay available when a single part fails. A photo should still exist after an upload reports success.
The answers chosen here are stated plainly. Photos are the product, so storage and viewing come first. The design accepts eventual consistency for views, where copies catch up a moment later instead of changing at the same instant.
Estimate the scale
The estimate starts from the stated size of the app. The app has 50 million registered users, and 5 million of them visit each day. Each daily user uploads two photos a day, and an average photo weighs 200 KB. Each photo is viewed 20 times on average.
The scale estimate, computed step by step
registered = 50000000
daily_users = 5000000
uploads_each = 2
photo_kb = 200
views_each = 20
seconds_day = 86400
photos_day = daily_users * uploads_each
kb_day = photos_day * photo_kb
tb_day = kb_day // 1000000000
tb_year = tb_day * 365
tb_day_copies = tb_day * 3
uploads_sec = round(photos_day / seconds_day)
views_day = photos_day * views_each
views_sec = round(views_day / seconds_day)
peak_sec = round(views_sec * 3, -2)
print(f"registered users: {registered:,}")
print(f"daily users: {daily_users:,}")
print(f"uploads per user per day: {uploads_each}")
print(f"photos per day: {photos_day:,}")
print(f"average photo size: {photo_kb} KB")
print(f"new storage per day: {tb_day} TB")
print(f"new storage per year: {tb_year} TB")
print(f"new storage per day with 3 copies: {tb_day_copies} TB")
print(f"uploads per second: {uploads_sec}")
print(f"views per photo: {views_each}")
print(f"views per day: {views_day:,}")
print(f"views per second: {views_sec:,}")
print(f"peak views per second: {peak_sec:,}")Read the lines as one story. The app takes in 10,000,000 photos a day, an average of 116 uploads per second. New photos add 2 TB a day and 730 TB in a year. Keeping three copies raises new storage to 6 TB a day. Views reach 200,000,000 a day, an average of 2,315 per second. Peak viewing reaches about 6,900 views per second.
The pattern matters more than any single line. Uploads arrive at a steady, modest rate. Storage grows every day and never shrinks back. Views dwarf uploads, so serving views cheaply is the central problem of this design.
High-level design
Clients start every flow. A client is the phone app or the browser in a person's hands. It sends each request and shows each response.
DNS finds the service. DNS is the lookup that turns a service name into the address of the service. The client asks DNS first, then sends its request to that address.
The load balancer spreads the load. It passes each incoming request to one of many app servers. When an app server stops working, the balancer sends its share to the healthy ones.
App servers do the answering work. An app server is the program that takes a request and builds the response. App servers are stateless, which means they keep no lasting record of earlier requests. Any app server can take any request, and more can be added as traffic grows.
Object storage holds the photos. It keeps each photo as one whole stored object under a name. It is built to hold a very large number of objects and to keep them safely.
The database holds the records. A record is one small row of facts, such as the owner of a photo and where that photo is stored. This design uses a relational database, because accounts and photos have clear links to each other.
The cache holds fast copies. A cache keeps copies of popular data in memory, where reads are quickest. A request the cache answers never reaches the database.
The CDN holds copies near viewers. A CDN is a content delivery network, a set of storage points spread across many places. A viewer downloads a photo from a nearby point instead of the home data center.
The queue and the workers handle slow jobs. A queue is a waiting line of jobs. A worker takes the next job from the queue and does it away from the request, such as making smaller copies of a new photo.
The upload path and the view path, in order
upload_path = [
"client sends the photo in a request",
"DNS gives the client the service address",
"load balancer passes the request to an app server",
"app server stores the photo in object storage",
"app server writes the photo row in the database",
"app server puts a resize job on the queue",
"worker makes the smaller copies",
]
view_path = [
"client asks the CDN for the photo",
"CDN serves its copy when it has one",
"on a CDN miss, an app server builds the answer",
"app server checks the cache before the database",
"database reads come from a read replica",
]
print("Upload path:")
for number, step in enumerate(upload_path, 1):
print(number, step)
print("View path:")
for number, step in enumerate(view_path, 1):
print(number, step)Each part does one kind of work. The parts meet only at the two paths above.
Uploading a photo
Uploading starts on the phone. The person picks a photo and taps upload. The client sends the photo in one request, and the load balancer passes it to an app server.
The app server stores the photo first. It writes the photo bytes into object storage under a fresh name. Only stored bytes count, so the later steps build on a photo that exists.
The app server then writes the record. The database row names the owner, the upload time and the stored location of the photo. An index is a lookup list the database keeps sorted by owner. A profile page uses that index to find one person's photos quickly.
Resizing waits for the background. The app server puts a resize job on the queue instead of shrinking the photo inside the request. A worker later makes the smaller copies used on small screens. The upload can report success before that job runs, because the original photo is already stored.
The upload is idempotent, so a retry with the same upload ID stores the photo once and not twice.
Viewing photos
Viewing starts at the edge. The viewer asks the CDN for the photo, and a nearby CDN point serves its copy when it has one. Most views end at this step and never travel to the home data center.
A miss goes deeper. The app server checks the cache for the photo record first. On a cache miss, it reads the record from a read replica. A read replica is a copy of the database that serves reads, while the leader takes the writes.
Profile pages arrive in pages. Pagination means the app sends one page of photos at a time instead of the whole history at once. A viewer who wants more asks for the next page.
Reads outnumber writes here because of the audience. Each photo is viewed 20 times on average, while it is uploaded once. That is 200,000,000 views a day against 10,000,000 uploads a day.
One deep dive: the hot photo
A famous account posts a new photo. A crowd of followers opens it within minutes, and they all ask for the same single photo.
That crowd creates a hot spot. A hot spot is one item that draws far more traffic than the items around it. Left alone, every one of those views would land on the same database copy.
The copies absorb the crowd instead. The CDN serves the photo from points near the viewers. The cache serves the photo record from memory for the requests that reach the app servers. The database underneath stays almost untouched.
One hot photo, with and without a cache
requests = 1000
photo = "sunset-42"
reads_without = 0
for _ in range(requests):
reads_without += 1
cache = {}
reads_with = 0
hits = 0
for _ in range(requests):
if photo in cache:
hits += 1
else:
reads_with += 1
cache[photo] = "photo data"
print(f"requests for one hot photo: {requests:,}")
print(f"database reads without a cache: {reads_without:,}")
print(f"cache hits: {hits:,}")
print(f"database reads with a cache: {reads_with}")The demo models the cache alone over 1,000 requests for the same photo. Without a cache, the database reads the photo 1,000 times. With a cache, 999 requests hit the cache and the database reads the photo once.
Wrap-up and trade-offs
The design in short is this. Photos live in object storage, and their records live in a relational database. Stateless app servers sit behind a load balancer, and a CDN plus a cache carry the views. A queue and workers prepare smaller copies in the background.
Two weak parts deserve honesty. Read replicas can lag behind the leader, so a viewer may briefly see a profile that is a moment behind the newest upload. The whole design also sits in one region, so a failure of that region would take the app down with it.
The next hour would widen the safety net. Copies of the photos and the database would go into a second region. Failover, the switch to a healthy copy when the main one fails, would be planned and practised before it is needed.
Every choice in this design trades a little freshness for a great deal of read capacity. That trade fits an app where viewing is the main event.
Exercise
In this design, a popular photo is viewed again and again. Which parts serve most of those views before the database is reached?