study
study
Books you read, turned into labs you run. Each exercise makes you predict a specific behavior, run the real thing, and check your prediction against a real transcript — the gap is where the mental model gets built.
Designing Data-Intensive Applications
Kleppmann & Riccomini, 2nd ed. (2025).
Chapter 2 · Defining Nonfunctional Requirements
-
Tail-Latency Amplification
— a backend call slow just 1% of the time, fanned out to 100 parallel calls, makes 63% of end-user requests slow. Predict "still ~99% fast"; measure ~37%.
-
The Mean Hides the Tail
— for right-skewed latency, 69% of requests are faster than "average," yet p99 is 6× the mean. The average overstates the typical request and understates the tail at once.
-
Averaging Percentiles Is a Lie
— average ten servers' p99s and you land 21% below the true pooled p99. Predict "close enough"; measure the gap.
Chapter 3 · Data Models and Query Languages
-
ADD COLUMN Is Instant, Until It Isn't
— adding a
NOT NULL column with a default to a 10M-row Postgres table takes 0.8 ms, not the full rewrite everyone predicts — but a volatile default rewrites it in ~8 s.
-
Reachability Needs Recursion
— "is Boise in North America?" A two-level join silently answers no (it's 4 hops deep);
WITH RECURSIVE gets it right at any depth. A fixed join count can't express transitive reachability.
-
One Company, Two Names
— a denormalized rename touches 666,666 rows vs 1; interrupt it and one company splits into two names — an inconsistency the normalized schema can't represent.
-
The Document Wins the Whole Read and Loses the Field
— a JSONB document reads ~20× fewer pages than the join for a whole profile, but ~12× more for a single field. Locality cuts both ways.
-
You Can Index Inside a Document
— a nested-field filter scans 2M rows (~98 ms); a GIN index makes it ~27× faster — with the catch that it serves
@>, not ->> equality.
-
The Ratings Matrix That's 99% Empty
— a 1%-full ratings matrix is 200 MB dense vs 4 MB sparse (50×), following
1/(2×density). Netflix-scale dense is 34 GB.
Chapter 4 · Storage and Retrieval
Chapter 5 · Encoding and Evolution
-
The Same Record, 81 Bytes to 32
— one record encoded five ways: JSON 81 → MessagePack 66 → Protobuf 33 → Avro 32 bytes. Each rung drops a layer of self-description; 38% of the JSON is field names.
-
Compression Closes the Gap
— Avro is 2.7× smaller than JSON raw, but only 1.4× after gzip — the binary size win largely collapses once you compress a batch.
-
The ID That Changed on the Way In
— a 64-bit ID through a double-based JSON parser comes back off by 21, no error; a typed Protobuf
int64 round-trips it exactly.
-
Schema Evolution: Old Code Reads New Data, New Code Reads Old
— add a field and old and new readers still interoperate (forward + backward compatible) — until you add one without a default, which breaks old data.
-
The Field-Tag Landmine: Reuse a Tag, Silently Corrupt
— reuse a Protobuf tag number and old bytes decode as the wrong field — a timestamp silently read as a user id, no error. A fresh tag is the only safe move.
-
Varint & Zigzag — −1 Costs 10 Bytes, or 1
— a negative
int64 sign-extends to 10 bytes; sint64 zigzag makes it 1. A batch of small negatives is 7.3× larger as int64.
-
Avro Is the Smallest — and Unreadable Without Its Schema
— Avro (21 bytes) beats Protobuf (24) by dropping tags, but decode with the wrong schema and it errors; Protobuf's tags survive a partial, renamed schema.
Chapter 6 · Replication
-
Your Own Write, Gone From the Replica
— write to the leader, read an async replica, and your own comment comes back missing (count 0) for ~5 s; reading the leader is the fix.
-
Refresh the Page, and the Comment Is Gone
— two replicas at different lag make a user's reads run backward in time (1 then 0); sticky routing gives monotonic reads.
-
The Same SQL, Two Different Rows
— a follower replaying
INSERT … now(), random() gets different values than the leader — why databases ship rows/WAL, not statements.
-
Last-Write-Wins Discards the Write That Actually Happened Last
— under a skewed clock, LWW keeps an older write and silently drops the one that truly happened last — no error, data gone.
-
Quorums Overlap by w + r > n — And Still Read Stale
—
w + r > n forces the read and write sets to overlap (9/9 combos), yet a sloppy quorum still returns stale.
-
Version Numbers Catch the Conflict LWW Hides
— two concurrent writes: LWW keeps
[eggs] and loses milk; version numbers keep both as siblings, losing nothing.
-
A G-Counter Converges; a Naive Counter Loses 80 Increments
— a naive merge keeps 100 of 180 increments (loses 44%); a G-Counter CRDT converges to the exact 180 under any merge order.
Source: github.com/atharh/study · each exercise's .md is the source of truth; the .html is its rendered companion.