Systems · 30 min

System design: the core trade-offs

Build a service up on the simulator: a cache and what it costs in staleness, replicas and lag, sharding keys, queues for slow writes, and what happens when a machine dies.

01Why it matters

Every fix buys something and costs something

A service that answers a thousand requests a second on two servers and one database is easy. Ten times the traffic is not, and every way of handling it trades something away. Add servers, and the load moves to the next part in line. Add a cache, and some reads come back out of date. Add database replicas, and they lag behind. Add a queue, and writes are acknowledged before they have happened. System design is choosing which of those costs your system can live with.

This lesson builds those choices on a simulator. Every lab below runs real traffic through a design, request by request: the cache really evicts, the queue really fills up, and a crashed machine really drops what it was doing. Change one thing at a time, run it, and watch what moves.

02Key parts

The pieces you will see

bottleneck
The part of a system that is busiest, close to 100% of its capacity. Requests queue in front of it, so it sets the latency and the most the whole system can do.
utilisation
How much of the time a part is busy. Near 100%, waiting times explode, as the queueing lesson showed.
cache-aside
The application reads from a cache first and, on a miss, from the database, then puts what it read into the cache. Writes must update or delete the cached copy, or it goes stale.
replication lag
How far behind the primary a read replica is. A read from a replica can miss writes made in the last moments.
shard
One partition of the data. Each shard holds a slice of the keys on its own machines, so writes spread across shards.
backlog
Work waiting in a queue. If it keeps growing, the consumers can't keep up with what arrives.
failover
Promoting a replica to take over when the primary dies, so writes can carry on.

Each lab is a canvas: select a component to change it, add new ones from the row of buttons, and press Run. The labs use the same seed, so the numbers in the captions are the ones you will see when you change exactly what they say.

03See it work

Scaling out moves the bottleneck

Two app servers get 3,000 requests a second. Each request does a little work on an app server, then holds that server's worker while it waits for the database. Run it first: the app servers are 100% busy, and most requests time out.

Try this: Run it: about 315 requests a second get through and 90% fail, with the app servers at 100%. Select the app servers and raise the instances to 4, then 6, then 10, running each time. At 4 nothing fails but p99 is 846 ms; at 6 it is 359 ms and the database is 99% busy; at 10 it is 370 ms and the bill has gone from $410 to $970 a month.

Components

  • Clients, Clients: 3K req/s, 90% reads
  • Load balancer, Load balancer: round robin
  • App servers, App servers: 2 × app, 8 workers
  • Database, Database: primary only

Connections

  • Clients to Load balancer: requests
  • Load balancer to App servers: spread
  • App servers to Database: reads & writes

Select a component in the diagram to change its settings and connections, or add one above.

Run traffic

Crash a machine (optional)

Adding app servers helps until they stop being the busiest part. From 6 instances on, the database is the bottleneck, and more app servers just add more requests waiting for it. The findings under the results name the bottleneck after every run: read them before changing anything.

04Predict

Predict, then run: a cache in front

The same 3,000 requests a second, now over 10,000 keys with a few hot ones, and 4 app servers. Without a cache, this is the design that ran at a p99 of 846 ms above. Now reads go to a cache first, holding up to 2,000 keys. Writes still go straight to the database.

Before you run it, decide: how much of the database's work will the cache take away, and will every read still return the latest value?

Try this: Run it: 75% of reads hit the cache, the database drops to 40% busy, and p99 falls to 35 ms. But look at stale reads: 54% of reads returned an old value. Now select the app servers and send their writes to the cache (it invalidates, then writes the database): stale reads drop to 0%, and the hit rate to 67%. Then set the cache to write-through: 75% hits, still 0% stale. Finally try capacities of 500 and 10,000 keys: 58% and 82% hits.

Components

  • Clients, Clients: 3K req/s, 90% reads
  • Load balancer, Load balancer: round robin
  • App servers, App servers: 4 × app, 8 workers
  • Cache, Cache: 1 node × 2K keys, LRU
  • Database, Database: primary only

Connections

  • Clients to Load balancer: requests
  • Load balancer to App servers: spread
  • App servers to Cache: reads
  • App servers to Database: writes
  • Cache to Database: misses

Select a component in the diagram to change its settings and connections, or add one above.

Run traffic

Crash a machine (optional)

The cache took most of the reads off the database, which is the point of a cache: a hot key read a hundred times costs one database query. But writes that skip the cache leave the old copy in it, and every read of a popular key that has since changed returns the old value. Sending writes through the cache fixes that. Invalidating deletes the copy, so the next read misses and refetches it; write-through updates the copy in place and keeps the hit.

05Try it

Replicas, shards, queues and failures

Read replicas: load against freshness

3,500 requests a second, 95% of them reads, on one database. Read replicas copy the primary's writes and serve reads, so the primary only takes writes. They copy asynchronously, a little behind: here 1 second behind.

Try this: Run it: 38% of requests fail with the database 100% busy. Select the database and add one replica: no failures, but p99 is 434 ms with the replica 100% busy. Add a second: p99 36 ms, the primary 10% busy and each replica about half. Now look at stale reads: 31% of reads. Lower the replica lag to 100 ms (12% stale), then 10 ms (0.4%).

Components

  • Clients, Clients: 3.5K req/s, 95% reads
  • Load balancer, Load balancer: round robin
  • App servers, App servers: 6 × app, 8 workers
  • Database, Database: primary only

Connections

  • Clients to Load balancer: requests
  • Load balancer to App servers: spread
  • App servers to Database: reads & writes

Select a component in the diagram to change its settings and connections, or add one above.

Run traffic

Crash a machine (optional)

Replicas scale reads, not writes, and they trade freshness for it. A replica that is a second behind answers reads with a second-old view of the data, so a user who writes and then reads from a replica may not see their own write. The fix is not more replicas but choosing where each read goes: read your own recent writes from the primary, or wait for the replica to catch up. Synchronous replication removes the lag, at the cost of making every write wait for the replicas.

Sharding: spreading the writes

2,500 requests a second, 80% of them writes. Replicas don't help, because every write goes to the primary. Sharding splits the keys over several primaries, each owning a slice found by consistent hashing.

Try this: Run it: 77% fail with the database 100% busy. Select the database and set 2 shards: no failures, p99 52 ms, the shards 65% and 74% busy. Try 4 shards (31% to 43% each). Then select the clients and raise the key skew to 1.5, so a few keys get most of the traffic: with 4 shards their load becomes uneven, from 13% to 66%.

Components

  • Clients, Clients: 2.5K req/s, 20% reads
  • Load balancer, Load balancer: round robin
  • App servers, App servers: 6 × app, 8 workers
  • Database, Database: primary only

Connections

  • Clients to Load balancer: requests
  • Load balancer to App servers: spread
  • App servers to Database: reads & writes

Select a component in the diagram to change its settings and connections, or add one above.

Run traffic

Crash a machine (optional)

Sharding is the way to scale writes, and it is hard to undo: the shard key decides which data lives together. A query that needs data from every shard has to ask every shard, and a hot key lives on one shard however many there are. With skewed traffic, the busiest shard, not the average, sets the limit.

Queues: answering before the work is done

1,000 requests a second, half of them writes, and each write takes the database 40 ms. The app server waits for every write, so writes have a slow tail.

Try this: Run it: p99 is 167 ms, set by the slow writes. Add a queue: it connects to the database by itself. Then select the app servers and send their writes to the queue, and run: p99 drops to 31 ms, but the queue’s 4 consumers can’t keep up. About 4,000 writes are still waiting at the end, each applied 4.4 s after it was acknowledged on average, and 4.3% of reads are stale. Raise the consumers to 24: no backlog, and writes applied 45 ms later.

Components

  • Clients, Clients: 1K req/s, 50% reads
  • Load balancer, Load balancer: round robin
  • App servers, App servers: 6 × app, 8 workers
  • Database, Database: primary only

Connections

  • Clients to Load balancer: requests
  • Load balancer to App servers: spread
  • App servers to Database: reads & writes

Select a component in the diagram to change its settings and connections, or add one above.

Run traffic

Crash a machine (optional)

A queue lets the system answer at once and do the work later, smoothing bursts and decoupling the caller from slow work. But the work still has to be done. If the consumers handle less than the average arrival rate, the backlog grows without limit, and for every write in the queue, the rest of the system can't yet see it.

Failures: what a crash costs

Machines die. This design has 3 app servers and a database with one replica, at 1,000 requests a second. In the run panel, you can crash a machine part-way through.

Try this: Under “Crash a machine”, pick Database primary at 4 s, press Add crash and run: reads carry on from the replica, but every write fails from then on, 5.8% of all requests. Now select the database, turn failover on (a replica takes over after 2 s) and run again: 2% fail. Crash Database replica 1 instead: 0.1%. Crash App servers #1: almost nothing, since the load balancer routes around it.

Components

  • Clients, Clients: 1K req/s, 90% reads
  • Load balancer, Load balancer: round robin
  • App servers, App servers: 3 × app, 8 workers
  • Database, Database: primary + 1 replica

Connections

  • Clients to Load balancer: requests
  • Load balancer to App servers: spread
  • App servers to Database: reads & writes

Select a component in the diagram to change its settings and connections, or add one above.

Run traffic

Crash a machine (optional)

Redundancy only helps if something switches to it. Stateless app servers are easy: the load balancer stops sending to the dead one. A database primary holds state, so promoting a replica takes time, and any writes it hadn't yet copied may be lost.

There is a harder version of this choice. If the network splits the primary from some replicas, the replicas can either keep answering with what they have, which might be stale (choosing availability), or refuse until they hear from the primary (choosing consistency). That is the CAP theorem: during a network partition you get one or the other. Its everyday companion, PACELC, adds that even without a partition, replicas trade latency against consistency, which is exactly the replica lag you tuned above. Raft, from the consensus lesson, chooses consistency: a minority side refuses writes.

One more piece: a rate limiter at the front turns away requests over a set rate at once, with an error the client can retry. During a spike, that protects everything behind it from collapsing, so the rest of the traffic still gets fast answers. Try one in the sandbox.

06Check yourself

Check yourself

  1. Question 1 of 8

    The app servers are 60% busy and the database 99% busy. Which change is most likely to cut p99 latency?

    Your answer comes back in Review in a few days, so it sticks.

  2. Question 2 of 8

    An app reads through a cache but writes only to the database. What goes wrong?

  3. Question 3 of 8

    What is the difference between invalidating and writing through on a write?

  4. Question 4 of 8

    Read replicas lag 1 second behind the primary. A user writes, then immediately reads from a replica. What may they see?

  5. Question 5 of 8

    A database is write-bound. What spreads the writes?

  6. Question 6 of 8

    Writes go through a queue and the API replies as soon as a write is enqueued. Which is true?

  7. Question 7 of 8

    The primary crashes and there is no failover. What happens, with one read replica?

  8. Question 8 of 8

    In CAP terms, a replica cut off from its primary keeps answering reads with the data it has. What has it chosen?

07Explain it back

Say it in your own words

Pick three things you can add to a struggling service (more app servers, a cache, read replicas, shards, a queue) and say what each one buys and what it costs.

08Recap

What to remember

  • A system is as fast as its busiest part. Find the bottleneck, scale that, then look for the next one.
  • A cache takes load off the database, but its copies go stale unless writes invalidate or update them.
  • Read replicas scale reads and lag behind; sharding scales writes and makes hot keys and cross-shard queries harder.
  • A queue replies before the work is done. The consumers must keep up with the average rate, or the backlog grows.
  • Redundancy needs a switch-over: load balancers route around dead app servers, failover promotes a replica. In a partition, choose consistency or availability.

Put it together in the case studies, each with traffic, targets and a budget, and practise the numbers that come first in the estimation drills.

Back to path