System Design Bits

Support the project

System Design Bits is free and always will be. If it helped, a coffee helps back.

Buy Me a Coffee QR codeBuy me a coffee

Capstone·18 min read

Scaling a Photo App to a Million Users

Ten pieces, one photo-sharing app, and the exact break that makes each one necessary.

Big Idea

Scaling, load balancers, caching, CDNs, replication, sharding, queues, rate limiting, consistency, failover. Every one of them was added to some real system the moment something broke without it. Nobody designs the million-user architecture on day one, they grow into it one break at a time, and each fix buys capacity by taking on a cost. Knowing the list is not the skill. Knowing which break calls for which fix, and what it will charge you, is.

Why This Exists

Why A List Of Components Is Not A Design

▸

These ten terms are usually taught as a list of components, which makes them look like a checklist to apply. Each one is really a response to a specific symptom, and reaching for it before the symptom exists buys the cost with none of the benefit.

▸

A real system and an interview question ask the same thing: what breaks first, and what does fixing it cost. Someone who shards a 10,000-user database has answered a question nobody asked, and has given up cross-shard queries to do it.

▸

Growing one app from a single server to a million users puts the pieces in the only order that explains them, the order the breaks actually arrive in, so the reason for each one is still attached to it when you meet it again on its own.

Think Of It Like

A Food Cart That Became A Chain

One cart, one cook, one griddle. A queue forms down the block, so you buy a bigger griddle, which is scaling up. The griddle has a size limit, so you open a second cart, which is scaling out, and now somebody has to stand on the corner pointing people at the shorter line, which is the load balancer.

Then the three dishes everyone orders start getting cooked in advance and kept warm, which is a cache. Customers across town get a branch in their own neighbourhood instead of a long walk, which is a CDN. The paperwork that used to happen at the counter while the customer waited moves to a back office, which is a queue.

None of it was planned at the cart. Every piece arrived the week the previous setup stopped coping, and every piece added something new to go wrong: a corner with nobody on it, a warm tray of yesterday’s special, a back office that lost an order.

The Shape Of It

Day One, And It Is Already Correct

One server running the app and its databasePhones100 usersOne serverapp code + databaseResponse80ms
At a hundred users this is not a naive architecture, it is the right one. One moving part, one place to look when something breaks, and no network between the app and its data. Everything below is what happens when this stops being true, in the order it stops being true.

Interactive

Watch It Grow, One Break At A Time

One diagram, ten stages. Press play, or step through it. Nothing on the canvas ever moves, it only arrives, and the badge on each box is the stage that put it there.

Stage 1Scaling10,000 users

One server became three identical ones, each holding nothing of its own.

The architecture after stage 1, ScalingrequestsPhonesusers1App serversstateless, ×31Databaseone machine1
Stage ten is what a large architecture diagram usually is: not a design, but a list of things that once broke, drawn all at once so the order is no longer visible. The ten cards below are that order.

The Walkthrough

Ten Breaks, And What Each Fix Costs

1
Scaling10,000 users

What breaks

The single server sits at 100% CPU through the evening peak and response times climb from 80ms to four seconds. Nothing is broken, there is simply not enough machine.

The fix

Scale up first: a bigger instance, one restart, no code change. When the biggest available machine still isn't enough, scale out instead, running several identical servers that share the work.

What it costs

Scaling up has a hard ceiling and leaves one machine whose death is the whole outage. Scaling out means the servers can no longer keep anything of their own, so sessions and uploaded files have to move off local disk before it works at all.

↗Deep dive: Scalability: Vertical vs Horizontal Scaling
2
Load balancing3 app servers

What breaks

Three servers now exist and nothing decides which of them answers a request. One gets all the traffic, the other two idle, and when one dies requests keep being sent to it.

The fix

A load balancer in front of the three, spreading requests across them and health-checking each one, so a server that stops responding stops receiving traffic until it recovers.

What it costs

The balancer is now the piece that must not die, so it needs its own redundancy. It also makes sticky sessions tempting, which quietly undoes the statelessness the previous stage paid for.

↗Deep dive: Core Concepts of Load Balancing
3
Caching60,000 users

What breaks

Everyone opens the same trending posts. Every refresh runs the same query against the same rows, and the database is at 90% CPU while the app servers are half idle.

The fix

Redis between the app and the database, holding the popular results in memory. Most reads stop at the cache, and entries expire after a short while so the feed does not freeze in place.

What it costs

A second copy of the truth. Every path that writes those rows now has to invalidate the cached version, and when one forgets, the failure is silent: the system keeps serving a wrong answer, fast.

↗Deep dive: Caching Strategies & Cache Invalidation
4
CDNsUsers in 40 countries

What breaks

A user in Sydney waits four seconds for a photo that travels from Virginia. The query took 20ms. The other 3,900 was the image crossing the planet.

The fix

A CDN: servers in many cities, each keeping copies of your photos, so every download comes from the nearest one instead of from your origin.

What it costs

Now two caches have an opinion about every file. A long expiry plus a filename that never changes serves last week's image for a week, which is why uploaded and built assets get content-hashed names.

↗Deep dive: Content Delivery Networks (CDNs)
5
Replication250,000 users

What breaks

The database is the bottleneck again, and the cache cannot absorb this part: most traffic is people scrolling their own feeds, and a personal feed is a different query for every user, so there is nothing popular to reuse.

The fix

Read replicas, which are read-only copies. New posts go to the main database, the primary, which copies them to the replicas, and the replicas take the reading.

What it costs

Replication lag. A replica is always a moment behind, so a read can return a version of the world from just before the write that is already committed. That is stage nine's problem, bought here.

↗Closest deep dive: Implementing Database Sharding and Partitioning
6
Sharding1,000,000 users

What breaks

Reads are handled, but every write still lands on the primary, and at peak the uploads alone exceed what one machine can commit. There is no bigger instance left to buy.

The fix

Shard: split the rows across several databases using a shard key, usually the user ID, so each database owns a slice of the users and takes only that slice of the writes.

What it costs

Anything that spans the slices gets harder. A search across all users, a join, a transaction touching two users: each now needs several databases and code to stitch the answers together. A poorly chosen key puts the busiest users on one shard, which runs hot while the rest idle.

↗Deep dive: Implementing Database Sharding and Partitioning
7
Message queuesEvery upload

What breaks

Each upload resizes the photo into five sizes and notifies a few thousand followers. All of it happens before the response returns, so the person who pressed Share watches a spinner for nine seconds.

The fix

The server stores the photo, drops a job onto a queue, and replies immediately. Background workers pick the job up and do the slow part, and if a worker crashes halfway the job goes back on the queue for another one.

What it costs

The work is no longer finished when the response says so, which the interface has to admit: a photo that exists but has no thumbnail yet is a state that must be designed. And since a job can run twice, running it twice has to be harmless.

↗Closest deep dive: Architectural Styles: Monolith to Event-Driven
8
Rate limitingOne bad client

What breaks

A script hits the API three thousand times a second. It is not malicious, it is a scraper with no sleep in its loop, and it takes the p99 for every real user with it.

The fix

A limit per client, checked before the expensive work runs: so many requests per minute, with everything over the line rejected outright and told when to come back.

What it costs

The counter has to be shared across the fleet. Enforced per server, the real limit is your number multiplied by the number of servers, and a legitimate burst from a large customer now looks identical to abuse.

↗Deep dive: Rate Limiting
9
ConsistencyOne visible staleness

What breaks

Emma posts a photo, pulls to refresh, and it is gone. Nothing failed. Her read landed on a replica that had not received the copy yet, so it answered honestly with a world that did not contain her post.

The fix

Decide per feature rather than globally. A like count can lag by a second and nobody notices. Emma's own post cannot, so a read about a user's own content, just after that user wrote it, goes to the primary.

What it costs

Every strongly consistent read gives up the replica capacity you added in stage five and pays the primary's latency instead, so they are spent only where the staleness would actually be seen.

↗Deep dive: CAP Theorem & Trade-offs
10
Failover3 in the morning

What breaks

The primary's machine dies. Reads keep working off the replicas and cache, which makes it worse: the app looks alive while every upload, comment and like fails.

The fix

A standby kept in sync with the primary, and automatic promotion when the primary stops answering. On Amazon RDS Multi-AZ this typically takes one to two minutes.

What it costs

Those one to two minutes are an outage for every write. The standby costs as much as the primary while doing nothing, and a promotion that fires on a brief network blip instead of a real death is its own kind of incident.

↗Deep dive: Latency, Throughput & Availability
Read replicas and message queues do not have a page of their own here yet, so stages five and seven point at the nearest concept rather than a dedicated one.

Worked Example

Emma’s Photo, And The Replica That Had Not Heard

Stage five bought read capacity and stage nine pays for it. Here is the whole bill, in six messages: the write commits on the primary, the refresh is routed to a replica for the good reason that replicas are there to take reads, and the replica answers correctly and uselessly with a world from a few hundred milliseconds ago.

Read routed to a replica

Replication lag hiding a user’s own writeEmmaApp serverPrimaryReplicaupload photowrite, commitcopy in flight...refresh feedreadno such photo

Own content read from the primary

Routing a read-your-own-write to the primaryEmmaApp serverPrimaryReplicaupload photowrite, commitrefresh feedher own, just writtenreadthe photo
The fix is one routing rule, not a consistency setting. Everybody else’s feed, every like count, every follower total stays on the replicas and stays slightly behind, because nobody can tell. Only the reads that would expose the lag to the person who caused it go to the primary.

Reference

The Whole Growth Path

PieceNeeded the momentWhat it costs
ScalingOne machine is at its limitA ceiling, or statelessness
Load balancersMore than one server existsA new piece that must not die
CachingThe same read keeps repeatingA second copy to invalidate
CDNsUsers are far from your filesStale assets after a deploy
ReplicationReads outgrow one databaseReplication lag
ShardingWrites outgrow one databaseCross-shard queries and joins
Message queuesA request waits on slow workUnfinished state, and retries
Rate limitingOne client can hurt the restA counter shared fleet-wide
ConsistencyA stale read becomes visibleLatency, paid per feature
FailoverA dead machine stops writesA standby, and a minute down

Counterpoint

Most Apps Never Leave Stage Three

The ladder above is ten stages long because the story ends at a million users. Almost nothing ends at a million users. A single well-sized database with a cache in front of it comfortably serves a business with real revenue, and the honest version of most roadmaps stops somewhere around stage four and stays there for years.

Which makes the expensive mistake the opposite of the one people worry about. The risk is not failing to scale, it is paying a scaling cost before the break arrives: sharding a database that one machine would have carried, splitting a service to solve a problem a function call did not have, choosing a shard key before you know how the data is read. Those costs are permanent, and they are paid in every feature built afterwards.

The useful reading of this page is not top to bottom. It is: find the symptom you actually have, apply that stage, and leave the rest on the shelf until something breaks.

Seen In The Wild

Teams That Walked This Exact Ladder

▪

Instagram reached tens of millions of users on sharded PostgreSQL, splitting rows across many logical shards keyed by user ID, with Memcached in front absorbing the read traffic. The order of the ladder above is close to the order they actually walked it.

▪

Netflix does not rent a CDN for video, it ships its own Open Connect appliances into ISP data centres. That is stage four taken to its conclusion: the bytes start in the same building as the viewer.

▪

Amazon RDS Multi-AZ keeps a synchronous standby in a second availability zone and promotes it automatically, typically in one to two minutes, which is the realistic shape of stage ten rather than the zero-downtime version people assume.

▪

Nearly every photo or video you have uploaded anywhere went onto a queue: the response came back the moment the file landed, while resizing, transcoding, moderation and notifications ran behind it.

Key Points

What To Carry Forward

✓

Every piece here is a response to a specific break, not a rung on a maturity ladder. The skill is matching the symptom to the fix, not knowing the list.

✓

Reads and writes outgrow a database separately. Replicas fix reads, sharding fixes writes, and most applications need the first years before the second.

✓

A cache and a CDN both store copies but solve different waits: one skips repeated work, the other shortens distance.

✓

A queue does not make slow work fast, it moves it off the request path, which moves unfinished state into the interface.

✓

Consistency is a per-feature decision, not a database setting. A like count can lag, somebody's own post cannot.

✓

Every fix adds a cost, so system design is really choosing which cost your app can live with.

Common Mistakes

Where This Usually Goes Wrong

✕

Sharding at 10,000 users because the design doc said a million, paying for cross-shard queries years before one machine would have struggled with the writes.

✕

Scaling out before making the servers stateless, so sessions sit on local disk and a user who lands on a different server is silently signed out.

✕

Adding a cache with no invalidation plan, then chasing a 'random' wrong value that is really one write path nobody remembered to update.

✕

Reading from a replica immediately after writing to the primary, then filing the missing row as a database bug rather than the replication lag it is.

✕

Enforcing a rate limit per server instead of per client across the fleet, so the real limit is the configured number times the number of servers.

✕

Treating automatic failover as zero downtime, when a promotion still refuses writes for a minute or two and nothing in the stack retries them.

“I buried four bones in the garden against a winter that has never once arrived. One under the shed, one by the wet patch, two I can no longer locate at all. I have dug up the wrong one twice. The bowl, meanwhile, has been filled every single day of my life. I am told this is planning ahead. The bones are not better for being buried, and the garden is now full of holes.”

Try It Yourself

Find Your Own Stage

Take an app you work on or have built. Write down, in one sentence each, the symptom it has right now, which of the ten stages that symptom calls for, and what that stage would cost you in code you would have to write or maintain. Then name the next stage after it, and the symptom you would have to see before it would be worth paying for. If the honest answer is “stage one, and nothing is broken yet”, that is the correct answer and worth writing down too.

Quiz Review

Check your understanding

Question 1 of 6

An app has 10,000 users and the database is pinned at 90% CPU. Why is sharding the wrong first move?

  • ✓Because sharding is the fix for writes outgrowing one machine, and at 10,000 users almost nothing is write-bound. The load is reads, which a cache or a read replica absorbs without changing the data model at all. Sharding buys write capacity you will not use for years, and charges for it immediately: every query that spans users gets harder, joins and transactions stop being free, and a shard key chosen this early is chosen before you know the access patterns that have to live with it.