12

The future of data systems

No single tool does everything well, so real applications glue together a database, a cache, a search index, a warehouse and more. The final chapter argues for gluing them with a log: write each fact once to an ordered system of record, and derive every other view by replaying it. That makes the whole thing behave like one database turned inside out — views can be added, rebuilt and migrated gradually. Correctness then comes from end-to-end ids and deterministic derivation rather than distributed transactions, and from continually verifying what you store. Finally: the data is about people, and that comes with responsibility.

One log, many derived views — vs dual writes

system-of-recordderived-datalog-ordered-integration
Two clients update the same records. In the first mode, each client writes the database and then the search index itself; in the second, they only append to a log, which three views consume in order at their own pace.

Each client writes the database, then the search index

Aclient ABclient Bdatabase1: —2: —search index1: —2: —
database
user:1—
user:2—
search indexvs database
user:1—
user:2—
last write issuedintent
user:1—
user:2—

Each message takes a random network delay. Red = the index disagrees with the database and nothing in flight will ever fix it. Try: stop random writes, press "Race" a few times in dual-write mode until the two stores disagree for good; then switch to the log and crash the index consumer — it resumes from its offset and converges.

writes issued
0
messages in flight
0
keys: index ≠ db
0
0 permanently (nothing in flight)
index writes lost to crashes
0
DB has it, index never will

Reprocessing: migrating a view without a big bang

view-reprocessingkappa-architecturelambda-architecture
Product wants a new view (views per country) that needs the whole history. Replay the log into a second view next to the old one and shift reads over gradually — or, lambda-style, patch a periodic batch recomputation with an approximate streaming layer.

One log, two versions of the view side by side

offset 0head 400v1 (live)
old view v1: views per page▮ truth · ▮ v1
/home
104
/pricing
91
/docs
99
/blog
106
new view v2: views per country▮ truth · ▮ v2
BR
0
US
0
DE
0
IN
0
00.510tickv2 lag (events)

v2 is a new consumer of the same log, starting at offset 0. v1 keeps running, so the switch is reversible at every step. Try: route 30% of reads to v2 before deploying it (wrong answers), deploy it, wait for the lag to hit zero, then ramp the router to 100%.

events in log
400
v2 lag
not deployed
replaying history
bad reads (last 200)
0
routed to an incomplete v2
implementations
1
same stream code for history and live

Unbundling the database: a dataflow you can rewire

unbundling-the-databasederivation-functionstream-triggersreads-as-events
Inside a database, an index is a structure kept up to date from the write stream. Unbundled, each index, cache and materialized view is an independent consumer of the log — and so is a trigger that sends emails.
when wiring the trigger, start at

Click a derived view to wire or unwire it from the log

applicationappends eventsevent log · offsets 14–2314Chen $6015Chen $5016+Eli @LIS17Dara $6018Eli→BER19+Femi @BER20Femi $9021Bruno $9022+Gus @LIS23Femi $70users tableoffset 24 ✓latest row per user idsecondary indexunwiredcity → userssearch indexunwiredname prefix → usersmaterialized viewunwiredrevenue per city (join orders ⨝ users)triggeroffset 24 ✓send a welcome email on signup
users tableAna·NYC Bruno·LIS Chen·BER Dara·LIS Eli·BER Femi·BER Gus·LIS
trigger7 sent · last: Eli, Femi, Gus

Each derived view is just a consumer with its own offset: creating an index means "replay the log from 0, then keep following". Try: wire the secondary index and watch it backfill; press "Ana moves city" and see the index and the revenue view update. Then unwire the trigger, switch to "offset 0" and rewire it: every old user gets a second welcome email.

events in log
24
the only thing anyone writes
views wired
2 / 5
max view lag
0
backfill, then live
duplicate emails
0
side effects don't replay safely
Reads can join the dataflow too. Put read requests on a stream and answer them with a stream–table join against the derived state, and you get a log of exactly what each user was shown — useful for causality ("what did they see before they bought?") and auditing, at the cost of more writes.

The end-to-end argument: retrying a money transfer

end-to-end-argumentoperation-identifier
The client sends a $10 transfer; requests and responses get lost. Without retries money silently fails to move; with blind retries it moves twice; with a request id carried from the client into the database, it moves exactly once.

Sequence of transfer req-003

same network luck in every mode
client (browser)app serverdatabasePOST $10BEGIN; debit; credit; COMMITcommitted200 OK
Outcome: client shows success; money moved 1×.
database rows written (latest last)
001002003

TCP already removes duplicate packets, and the database already makes each transaction atomic — but the retry is a new connection and a new transaction, so neither layer can tell it is the same transfer. Only an id minted by the client and checked in the database (end to end) can. Try: with "retry on timeout", send 20 and count the red rows; switch modes — the same losses happen, with different results.

transfers requested
3
intended $30
money actually moved
$30
double charges
0
never happened
0
client gave up
'error' but it went through
0
the user retries by hand…

Constraints without coordination: timeliness vs integrity

uniqueness-via-logtimeliness-vs-integritycoordination-avoidanceapology-compensation
Users race to claim usernames. Route every request for a name to one partition of a log and decide in order — or let replicas say yes from their local copy and discover the conflict later.

Requests routed by hash(username) to a single-threaded partition

sign-upsclaim a namepartition 0 · log1×claimed (0)—partition 1 · log1×claimed (0)—partition 2 · log1×claimed (0)—

All requests for the same name hash to the same partition and are decided one at a time in log order: no locks, no distributed transaction, never two winners. Slowing the processor only delays the answer. Try: race three users for one name in each mode; in the replica mode raise the lag. In the log mode crank the processing delay — the answer gets later (timeliness), but it is never wrong (integrity).

accepted / rejected
0 / 0
avg time to answer
0.0 ticks
timeliness
duplicate usernames
0
integrity violations → apologies
queued requests
0
"Consistency" mixes two different things. A late answer (timeliness) fixes itself if you wait; a wrong answer (integrity) stays wrong until someone repairs it. Logs plus deterministic processing give you integrity cheaply and let you trade only timeliness for availability and speed.
Twelve account balances derived from a hash-chained event log and stored with checksums. Inject a fault, then see which audit catches it.
inject:audit:

Event log (hash-chained) → derived account balances (checksummed blocks)

a6+10cd69a0-4088c0a0-40d73ba1+0e257a6+801880a7+60af8ba3-20199ba6+1012dba6+409c54a1+5034a1a5+50cddba4-307fcda8+106f1ba1+069ada0-40114aa3+800629
acct a0
$-20
sum dfc930
acct a1
$150
sum f52c65
acct a2
$100
sum 6733d4
acct a3
$160
sum fb2ead
acct a4
$70
sum 8fe5d0
acct a5
$150
sum f52c65
acct a6
$240
sum 2bdd6d
acct a7
$160
sum fb2ead
acct a8
$110
sum 6d361c
acct a9
$100
sum 6733d4
acct a10
$100
sum 6733d4
acct a11
$100
sum 6733d4

C = checksum, H = hash chain, R = re-derivation. Each audit catches a different class of fault: checksums catch storage corruption but happily vouch for a bug's output; the hash chain catches history being rewritten; recomputing derived state from the log catches both the disk and the bug. Try: inject a buggy write, run the checksum audit (clean!), then re-derive.

faults injected
0
caught
0
still undetected
0
silent corruption spreads to every derived view
audits run
none
The same pipelines that keep views fresh can track people, feed opaque automated decisions, and accumulate a dataset nobody would consent to if they saw it whole.

What you collected for a feature vs what it can reveal

location trail
collected to: show nearby stores
  • → can infer home and work address
  • → can infer visits to clinics or places of worship
  • → can infer who you spend nights with
purchase history
collected to: order receipts
  • → can infer pregnancy, before you tell family
  • → can infer financial distress
  • → can infer health conditions

Joined together, these datasets reveal more than any one of them — and a join is one query away.

Every stored record outlives the intent it was collected with: the company may be acquired, breached, or compelled. Try: switch on browsing and set retention to 10 years, then ask whether the feature really needed more than a week.

records held
111.0B
all of it exposed in one breach
sensitive inferences
6
kept for
1 yr
data as liability, not asset
A reflective exercise: replace "data" with "surveillance" in the sentences you use at work ("we built a surveillance pipeline to improve engagement"). If it sounds wrong, ask who benefits, whether users meaningfully consented, and what happens when a model trained on yesterday's biases denies someone a loan with no way to appeal.
  • takeawayDecide what is the system of record and derive everything else from an ordered log of its changes. Consumers of one log can lag, but never disagree — unlike dual writes, which race and partially fail.
  • takeawayA derived view is a function of the log, so you can add one, rebuild one, or migrate gradually by running old and new side by side. One stream implementation can handle history and live data.
  • takeawayCorrectness is end to end: carry request ids into the database, enforce constraints by ordering requests in a partitioned log, prefer integrity over timeliness, and keep auditing — checksums alone won't catch your own bugs.

Key concepts · 20

book chapter 12
  1. System of record

    The authoritative copy of the data, where each fact is written first and exactly once.

    When two copies disagree, the system of record wins by definition. Being explicit about which dataset is authoritative and which are derived is the first step to integrating many tools sanely.

    ↑ see it in the visualization
  2. Derived data

    Caches, indexes, materialized views and models computed from the system of record by some transformation.

    Derived data is redundant by design: if lost or wrong, you can recompute it. That property is what makes it safe to have many specialized stores for one dataset.

    ElasticsearchRedisdata warehouse
    ↑ see it in the visualization
  3. Ordering writes through a single log

    Every change is appended to one totally ordered log; each derived system consumes it in that order.

    Ad-hoc dual writes from the application race and partially fail, leaving stores permanently inconsistent. With a log, all consumers apply the same writes in the same order, so they can only be behind, never contradictory — they converge.

    KafkaDebezium CDCPulsar
    ↑ see it in the visualization
  4. Reprocessing to evolve a view

    Building a new version of a derived view by replaying the full history alongside the old one.

    It turns schema migrations into a gradual, reversible process: run both views, shift a fraction of reads to the new one, and roll back at any point because the old view never stopped.

    ↑ see it in the visualization
  5. Lambda architecture

    A batch layer periodically recomputes exact views from immutable events; a speed layer serves fast approximate updates in between.

    It combined reliable batch reprocessing with low-latency streams, but you implement the logic twice in two frameworks and must merge their outputs, and the approximate path drifts between batch runs.

    Hadoop + Storm
    ↑ see it in the visualization
  6. Unifying batch and stream (kappa)

    One stream processor handles both live events and the replay of history from the beginning of the log.

    With a log that retains history and engines that support event time and exactly-once semantics, the same code can rebuild a view from scratch or keep it up to date — one implementation instead of two.

    Kafka StreamsFlinkBeam
    ↑ see it in the visualization
  7. Unbundling the database (“database inside-out”)

    Treating the log as the database's core and its indexes, caches and views as separate derived consumers.

    A database already does this internally: CREATE INDEX scans existing data then follows new writes. Unbundling exposes that mechanism so you can compose specialized storage and processing tools, loosely coupled through the log.

    ↑ see it in the visualization
  8. Application code as a derivation function

    Expressing derived datasets as functions from input streams to output state, run by stream processors.

    Instead of application code reading and writing a shared mutable database, each view is a deterministic function of the log. Adding a view means wiring a new consumer, not changing the writers.

    ↑ see it in the visualization
  9. Stream processors as triggers

    Consumers that perform side effects (emails, notifications, calls to external services) in response to events.

    Unlike pure views, side effects don't replay safely: rebuilding from offset 0 sends every email again. Side-effecting consumers need to start at the head or deduplicate on event ids.

    ↑ see it in the visualization
  10. Reads are events too

    Logging read requests into a stream and answering them with a stream–table join against the derived state.

    It records exactly what each user saw and when, which helps tracking causality and auditing; it also lets reads be routed to the partition holding the data, like a distributed query.

    ↑ see it in the visualization
  11. The end-to-end argument

    Some guarantees can only be implemented correctly with the help of the application at both endpoints of the communication.

    TCP removes duplicate packets and a transaction is atomic, but neither knows that a retried HTTP request is the same transfer. Lower layers' guarantees help performance but don't remove the need for an end-to-end check.

    ↑ see it in the visualization
  12. Operation identifiers for duplicate suppression

    A unique request id minted by the client and carried all the way to the database, enforced with a uniqueness constraint.

    Retrying is the only way to survive a lost response, and only an end-to-end id makes a non-idempotent operation like a money transfer safe to retry: the second attempt finds the id and does nothing.

    Stripe Idempotency-Keyrequests table with UNIQUE(request_id)
    ↑ see it in the visualization
  13. Uniqueness constraints via a partitioned log

    Route all requests for the same value to one partition and decide them sequentially in log order.

    A single-threaded processor per partition decides deterministically which request came first, with no locks or distributed transactions, and scales by adding partitions.

    ↑ see it in the visualization
  14. Timeliness vs integrity

    Timeliness: readers see an up-to-date state. Integrity: no corruption, no contradictory or lost data.

    Violations of timeliness are temporary — wait and they heal. Violations of integrity are permanent until someone repairs them. Most applications need integrity strictly and can tolerate some lag.

    ↑ see it in the visualization
  15. Coordination-avoiding data systems

    Systems that preserve integrity using logs, deterministic derivation and end-to-end ids instead of synchronous cross-node coordination.

    They keep working across datacenters and network faults, with better performance, while giving up only timeliness. Synchronous coordination can be reserved for the few places where it truly cannot be avoided.

    ↑ see it in the visualization
  16. Compensating transactions (apologies)

    Fixing a constraint violation after the fact: refund, reassign, or apologize to the customer.

    Many businesses already tolerate brief violations (overbooked flights, oversold stock) because a cheap apology beats the cost of strict coordination. The question is how often it happens and how expensive each apology is.

    ↑ see it in the visualization
  17. Trust, but verify (auditing)

    Continuously checking the integrity of data — checksums, re-deriving views from the log, comparing replicas.

    Disks rot, software has bugs, and checksums can vouch for a bug's output. Systems that are designed to be audited (immutable events, deterministic derivation) let you detect corruption early instead of trusting blindly.

    HDFS block scannerZFS scrubCassandra repair
    ↑ see it in the visualization
  18. Cryptographic auditing

    Chaining each log entry to the hash of the previous one (or using Merkle trees) so tampering is detectable.

    Rewriting an old event breaks every hash after it. The same idea underlies certificate transparency and blockchains — a way to make the log's integrity verifiable by others.

    Certificate TransparencyMerkle treesGit
    ↑ see it in the visualization
  19. Predictive analytics and responsibility

    Automated decisions (loans, insurance, hiring, policing) driven by models trained on data.

    Models can codify existing discrimination and create feedback loops, and the people affected rarely can appeal or even learn why. Engineers who build these systems share responsibility for their consequences.

    ↑ see it in the visualization
  20. Privacy, tracking and data as liability

    Behavioural data collected for one feature can reveal far more, and lives on beyond its purpose.

    Every stored record can be breached, subpoenaed or repurposed after an acquisition. Collect less, keep it shorter, and treat data as a hazardous material rather than an asset.

    ↑ see it in the visualization