Matías Fernández / System Design DDIA · CHAPTER 01

System Design DDIA Chapter 01

Reliable, scalable,
maintainable.

The opening chapter does something quietly ambitious: before discussing any database, index or queue, it argues that almost every meaningful decision in a data system can be traced back to three concerns. Read the remaining eleven chapters with that lens and most techniques turn out to buy one of them, usually at the expense of another.

PARTI · Foundations
CONCERNS3
CENTRAL CASETwitter fan-out
READING~15 min

01 / THE PREMISE

Data-intensive,
not compute-intensive.

The systems most of us build are rarely limited by raw CPU. They are limited by the amount of data, its complexity, and the speed at which it changes.

That shift has a consequence the chapter draws out carefully. No single tool covers the whole job any more, so we assemble systems out of parts: a database for the source of truth, a cache for the expensive reads, a search index for queries the database is bad at, a queue for work that should not block the request, a stream or batch processor for everything derived.

The moment you put an API in front of that assembly, something changes that is easy to miss. You have not integrated tools — you have built a new data system, and its guarantees are now yours. The cache does not promise it agrees with the database. The search index does not promise it is current. Whatever consistency your users experience is a property you designed, deliberately or not.

FIGURE A The boundary you become responsible for
CLIENT

YOUR SERVICE — one API, one implied set of guarantees

DATABASEsource of truth CACHEmay be stale SEARCH INDEXeventually current QUEUEasync, retried

No component promises the others agree with it. That promise is the one you make.

01

Reliability

It keeps working correctly when something goes wrong — hardware, software, or a person.

02

Scalability

There are reasonable options for keeping performance acceptable as load grows.

03

Maintainability

People can operate it, understand it, and change it without fear — for years.

02 / RELIABILITY

Continuing to work correctly
when things go wrong.

The definition is doing real work. Not "nothing goes wrong" — things will go wrong. The claim is that the system keeps performing its function anyway.

"Working correctly" is broader than it sounds. The chapter unpacks it into four expectations: the application does what the user expected, it tolerates users making mistakes or using it in unexpected ways, its performance is good enough for the use case under the expected load, and it prevents unauthorised access and abuse.

The distinction the chapter insists on is between a fault and a failure. A fault is one component deviating from its spec. A failure is the system as a whole no longer providing the service the user needs. They are not the same event, and the whole discipline lives in the gap between them: the probability of a fault can never be zero, so fault tolerance means arranging things so that a fault never gets to become a failure.

FIGURE B The gap where fault tolerance lives
FAULTreplica goes down requests pile up threads exhausted FAILUREservice is down
FAULTreplica goes down health check ejects it traffic reroutes CONTAINEDdegraded, still serving

Same fault, two systems. The difference is not luck — it is whether this specific fault was named in advance and given a path that stops short of the user.

There is a counter-intuitive move here that is worth sitting with: the way to trust fault tolerance is to deliberately increase the rate of faults. Netflix's Chaos Monkey kills processes at random without warning, and that is not recklessness. Many critical bugs live in poor error handling, and injecting faults is what keeps the recovery path exercised. An untested recovery path is not a recovery path.

Tolerance is not always the right answer, though. Some faults have no cure, only prevention: if an attacker has compromised a system, that event cannot be undone. The chapter generally prefers tolerating faults over preventing them, but security is where that preference stops.

Hardware faults

Random and mostly independent. One machine failing says little about the next.

LOOKS LIKE

A disk dies, RAM develops a bad cell, a rack loses power, someone unplugs the wrong cable. Disks have a mean time to failure of roughly 10–50 years, so a cluster of 10,000 disks should expect about one dead disk per day.

WHAT ACTUALLY HELPS

Redundancy first: RAID, dual power supplies, hot-swappable CPUs. But as data volumes grow, and cloud platforms trade single-machine reliability for elasticity, the answer shifts to software that tolerates the loss of whole machines. That buys something extra: you can patch and reboot nodes one at a time, with no planned downtime.

Software faults

Systematic and correlated. The same latent bug lives on every node and fires on every node at once.

LOOKS LIKE

The leap second of 30 June 2012, when a Linux kernel bug hung many applications at the same moment; a runaway process exhausting a shared resource; a dependency that slows down or returns corrupted responses; cascading failures where one fault triggers the next.

WHAT ACTUALLY HELPS

No single countermeasure. Test the assumptions, isolate processes, let things crash and restart, and measure behaviour in production so a drifting system announces itself.

Human faults

The most frequent source in practice. In one study of large internet services, operator configuration errors were the leading cause of outages; hardware played a role in only 10–25% of them.

LOOKS LIKE

A config change rolled out everywhere at once, a migration run against the wrong environment, a manual step skipped at 3am.

WHAT ACTUALLY HELPS

Design the opportunity for error out, rather than blaming the humans: well-designed abstractions, realistic sandboxes, tests at every level, gradual rollouts, fast rollback, and detailed telemetry that shows what actually happened.

03 / SCALABILITY

"Is it scalable?"
is the wrong question.

Scalability is not a property a system has or lacks. The useful version is a conditional: if load grows along this specific dimension, what are our options?

Which makes the first task descriptive rather than architectural. You have to say what "load" means here, as a number: requests per second, ratio of reads to writes, number of simultaneously active users, hit rate on the cache. The chapter calls these load parameters, and choosing the wrong one is how teams end up optimising something that was never the bottleneck.

TYPICAL LOAD PARAMETERS

requests / second read : write ratio simultaneously active users cache hit rate fan-out per write working set size

The chapter then spends its best pages on a single example, and it is worth following closely because the punchline is not the architecture — it is how the right load parameter was found. The figures come from a November 2012 Twitter talk that Kleppmann cites.

FIGURE C Two ways to build a home timeline
4.6ktweets posted / sec (average)
12ktweets posted / sec (peak)
300ktimeline reads / sec

Writes are not the problem. Reads outnumber them by roughly 65 to 1 — so the design question is where to pay for the join between "who I follow" and "what they posted".

APPROACH 1 · JOIN ON READ

Store every tweet in one collection

WRITEinsert 1 row GLOBAL TWEETS READjoin followees, sort, merge

Writes are trivial. But every one of the 300k reads per second pays for the join, and that is the operation happening 65 times more often.

APPROACH 2 · FAN-OUT ON WRITE

Precompute a mailbox per user

WRITEinsert into every follower TIMELINE CACHES READreturn a ready list

Reads become nearly free. The cost moves to write time, where there is 65× more headroom — which is exactly why Twitter moved in this direction.

THE COST OF APPROACH 2

4.6k tweets/sec ~75 followers on average 345k writes/sec into timeline caches

Still tractable. But this is an average, and the average is hiding the entire problem.

THE DISTRIBUTION IS THE LOAD PARAMETER

median user a handful of followers
average ~75 followers
celebrity 30M+ followers — 30M writes for one tweet

A single celebrity tweet would have to land in tens of millions of mailboxes, and the chapter's stated goal of delivering timelines within about five seconds makes that impossible to do naively. So the answer is a hybrid: fan out on write for the vast majority of users, and merge in the handful of high-follower accounts at read time.

The lesson is the method, not the architecture. The load parameter that decided this design was not requests per second — it was the distribution of followers per user. Averages chose the wrong architecture; the tail chose the right one.

Describing performance once load is defined

Once load is described, you can ask what happens when it grows, and the chapter phrases that as two concrete questions. If load increases and resources stay the same, how does performance change? And if load increases, how much do resources have to grow to keep performance where it was?

Both questions need a number for "performance". Batch systems care about throughput: records processed per unit of time. Online systems care about response time: what the user waits through.

And response time is not a number — it is a distribution. The same endpoint, called repeatedly with identical arguments, returns a spread of times: a context switch, a garbage collection pause, a lost packet and retransmission, a cold page. Which is why the chapter is blunt about averages: the mean does not tell you how many users actually waited that long. Percentiles do.

FIGURE D What percentiles say that an average cannot
p50 median half of users are faster than this
p95 1 request in 20 is slower
p99 1 in 100 — often your heaviest accounts
p999 1 in 1,000 — the tail Amazon chose to watch

fast RESPONSE TIME — shape is illustrative, not measured slow

The argument for caring about the tail is commercial, and the chapter cites it directly: the slowest requests often belong to the customers with the most data — that is, the most valuable ones. Amazon observed that 100 ms of extra response time cost about 1% in sales, and others reported a 1-second slowdown cutting a customer satisfaction metric by 16%. Amazon still drew a line at p999: optimising the 99.99th percentile was judged too expensive for the return.

Percentiles are also how performance becomes a contract. Service level objectives and agreements are usually written in them: for example, the service counts as up if the median response time is under 200 ms and p99 is under 1 s. Two practical traps follow. First, percentiles cannot be averaged: to combine data from several machines or time windows, add up the histograms and compute the percentile from the result. Second, a load-testing client must keep sending requests without waiting for the previous response, or it will hide the very queues it is supposed to measure.

Why tails get worse in distributed systems

Two mechanisms make this sharper than it first looks. The first is head-of-line blocking: a server handles a limited number of requests in parallel, so a few slow ones delay everything queued behind them. Requests that are fast to process still arrive late. This is why the measurement has to be taken on the client side, where the queueing is visible.

The second is tail latency amplification, and it is the one that surprises people. If a single user request fans out into several backend calls, the user waits for the slowest of them. Rare slowness stops being rare as fan-out grows.

FIGURE E Tail latency amplification
1 USER REQUEST
fastfastfast SLOWfast
The user waits for the slowest one. With enough parallel calls, hitting the tail stops being unlikely — it becomes the normal case.

Coping with load

Only now, with load and performance described, does the chapter allow the architectural question, and it deflates it on purpose. Scaling up (a bigger machine) and scaling out (more machines) are not a religious choice. Real systems use both, because a few powerful machines are often simpler and cheaper than a large fleet of small virtual ones. The same pragmatism applies to elasticity: adding resources automatically helps when load is unpredictable, while scaling by hand is simpler and has fewer operational surprises.

Distributing stateless services is largely mechanical. Distributing stateful data systems is where the difficulty concentrates, and that is why the common advice has long been to keep the database on a single node until cost or availability forces otherwise. Part II of the book is about what happens after that point. The closing warning is the one worth memorising: there is no generic scaling architecture. A system that handles 100,000 requests per second of 1 kB each looks nothing like one that handles 3 requests per minute of 2 GB each, even though both move the same amount of data. The architecture follows from the load parameters, and those are specific to your problem.

04 / MAINTAINABILITY

The majority of the cost
comes after it ships.

Most of the money a system consumes is spent on maintenance: fixing it, adapting it to new requirements, operating it, and paying down the debt of decisions made quickly. Nobody enjoys maintaining a legacy system, so the goal is to design one that will not become one.

Operability

Make routine work boring.

Give the people running the system visibility into its behaviour, predictable and documented defaults, and good support for the tasks they perform every week. Most of a system’s life is operation, not construction.

Simplicity

Remove the complexity that is not the problem’s fault.

Some complexity is essential — it comes from the domain. The rest is accidental: it comes from the implementation. Accidental complexity is the kind you are allowed to delete, and abstraction is the tool for deleting it.

Evolvability

Assume the requirements will move.

They always do: new features, new regulations, new scale, new platforms. A system that is simple and well understood is one you can change cheaply — which is why evolvability is mostly simplicity, seen over time.

The distinction inside simplicity is the one that earns its keep in code review. Essential complexity comes from the problem: tax rules really are that complicated. Accidental complexity comes from how we built it: tangled dependencies, inconsistent naming, special cases papering over a leaky abstraction. Only the second kind is negotiable, and separating them turns "this is too complex" from an opinion into a claim you can check.

05 / VOCABULARY

Four terms the rest
of the book assumes.

Chapter 1 is largely there to install these. The rest of the book uses them without re-explaining them.

Fault ≠ failure
A fault is one component deviating from spec. A failure is the system as a whole no longer providing the service the user needs. Tolerance means containing the first so it never becomes the second.
Load parameter
The number that describes what actually grows: requests per second, read/write ratio, fan-out, cache hit rate. Choosing the wrong one hides the bottleneck.
Tail latency
The p99 and p999 response times. An average hides how many users waited; the tail shows the slowest requests, which often belong to the customers with the most data.
Accidental complexity
Complexity that comes from the implementation rather than from the problem itself. It is the kind you are allowed to remove.

06 / IN PRACTICE

What to carry into
the next design review.

The chapter is conceptual, but it converts into four questions that change conversations.

  1. 01

    Name the load parameter before arguing about architecture.

    Requests per second, read/write ratio, fan-out, working set size. If the group cannot agree on which number is growing, the design debate has no ground truth.

  2. 02

    Ask for percentiles, never averages.

    A mean response time hides how many users actually waited. Ask for p50, p99 and p999, ask where they were measured (server-side numbers hide queueing that the client feels), and aggregate histograms, never percentiles.

  3. 03

    Separate the fault from the failure.

    For each dependency, ask what happens when it is slow, wrong, or gone. A design is fault-tolerant only for the faults you named out loud.

  4. 04

    Treat operations as a first-class requirement.

    Rollback path, deploy strategy, and the signals you will alert on belong in the design, not in a runbook written after the incident.