System-wide event sourcing: current state, CDC, an event lake, and precise moves against duplicates and disorder.
Why this book
A food delivery app is split into several services. The order service records an order. The driver service has to find out about it, then tracking has to show it to the customer. Meanwhile the network drops, one message arrives twice, another arrives late. One day the order service goes down, and customers still want to know where their meal is.
That food delivery app is the running example of John Gilbert's book. Gilbert is a CTO with more than thirty years in the trade. He tackles these failures in the serverless world: code the cloud provider runs on demand, with no server to manage, here AWS Lambda. His thesis, as I read it: each service keeps its own data, and services only talk through events that are kept as facts.
Gilbert sums it up in one line: "we are not just turning the database inside out, we are ultimately turning the cloud into the database" (lesson 54). Put plainly, each service has its own small database instead of one big shared one, and events tie those small databases together. Three words come up everywhere:
- Service: a small independent program with its own database, like a shop that runs its own till.
- Event: a message announcing that something happened, written in the past tense, such as "order placed" or "payment received".
- Eventual consistency: services don't agree at every instant, but they end up agreeing once the events have arrived.
For my part, I build event-sourced CQRS systems in Go for finance and crypto. I turned that work into a series of articles. Event sourcing keeps every change as an event instead of overwriting a value. CQRS (Command Query Responsibility Segregation) splits the code that writes from the code that reads, each with its own tables.
Gilbert does both, but not the way I do. That comparison is the thread of my reading. On ExpertEdge, Packt's subscription platform, I read eight of its thirteen chapters. My comparisons with my own practice are flagged "My reading".
I keep nine ideas from it, in two families. Ideas 1 to 4 show how a service records and publishes facts without depending on the others. Ideas 5 to 8 keep the system correct when things go wrong: duplicates, disorder, a failed step, a region going down. The last one protects those facts, all the way to the right to erasure.
The ideas that stay
1An eventually consistent system works like a busy coffee shop
First family: facts, starting with what they replace. In a busy coffee shop in the morning, one person taking orders, making the drinks and taking payment would send the line out the door. So the shop splits the work into stages. You pay at the till, you get a receipt, then you wait for your drink at the counter.
The receipt is the heart of the example: it states the fact that you paid (lesson 29). You know your drink will come, and the shop knows it must serve it or refund you. Nobody locked the till while the coffee was being made. The book draws its rule from this: "Facts are the lifeblood of an eventually consistent system" (lesson 30).
Each stage therefore performs a single atomic action, meaning all or nothing, then publishes a fact once it is done. The next stage reacts to that fact. The whole system moves forward through a chain of short actions.
The coffee shop has its failures too, and the book tells them. The crew loses your coffee and puts a new one at the head of the queue. Then they find the original, whose label was smudged: you now have two coffees. Gilbert's conclusion: "duplicate output is better than no output" (lesson 30). The next stage simply has to cope with duplicates.
The same shop doesn't guarantee order either. A simple drink comes out before a complicated one, and the customer sorts things out at the end. Programs that read events must do the same, because arrival order is never guaranteed (lesson 30). Both failures, duplicates and disorder, come back in idea 5.
2Gilbert's event sourcing keeps history outside the service
Those facts still have to be kept somewhere. Take a bank account. The book compares three versions of it (lesson 34):
- Without event sourcing: one row per account holding the balance, overwritten on every operation. The book admits it: "We are losing information."
- Traditional Event Sourcing: one row per operation, credit or debit. Nothing is lost, but reading the balance means rereading every operation on every request.
- System-wide event sourcing: the service keeps only the current balance, and every change is sent to the other services as a fact.
The third model is the book's own. Its pivot is CDC (Change Data Capture). CDC is the stream of changes a database publishes on every write. A small function in the service reads that stream and forwards each change to the event hub. The hub delivers events to every interested service. The book also calls it "the bus".
So the service itself keeps no history. The book hands that job to an event lake. It is file storage, one per subsystem, that receives every event and keeps it forever. For Gilbert, this lake "acts as the system-wide transaction log" (lesson 34).
The lake also makes replay possible. A bug made a service miss some events? You resend the missing period. A new service shows up? You feed it a few weeks or months of events. The book is firm: "we do not republish events", or every service would receive them again (lesson 35). They go to the one service that needs them.
In my Go event store, I do the opposite. Events are the source of truth, in a PostgreSQL table you only ever append to. The state of an aggregate, the business object that guards its own rules, is rebuilt by replaying its events in order.
Gilbert keeps a current state that gets overwritten, and the history lives further away, in the lake. His service stays simple. The trade-off: proof of what happened depends on CDC and the lake, not on the service. For a financial system, my question is therefore: in a dispute, which one is authoritative, the table or the lake? The book gives that role to the lake without discussing it.
3A service writes to one place only, then CDC publishes
Keeping the facts isn't enough: you also have to publish them without losing any. Picture a service that saves an order in its database, then sends it to the event hub. The program crashes in between. The database has the order, but nobody downstream will ever hear about it.
Hence the book's rule: "we cannot reliably save data to more than one data store at a time" (lesson 36). The reason lies in distributed transactions. The protocol that commits two writes together, two-phase commit, doesn't scale. So you pick a single place to write first:
- Database-first: the service writes to its database, and that database's CDC publishes the event. The book keeps it for services where the user must read back right away what they just wrote.
- Stream-first: the service publishes the event, and downstream services store it. The book keeps it for services with no direct user, or for a database without CDC.
The database-first case has a bonus (lesson 61). The function reading the CDC first publishes every raw change, with the row before and after. That stream is an audit log: what changed, when, and by whom. Only then does a rule pick out the real business event, for instance a status that turns to "submitted".
This is the problem from my article on the outbox. My answer there is an outbox table, written in the same transaction as the event, then a program that reads it and publishes. The book's CDC does without that table: it reads the database's internal log directly.
Outside AWS, the principle still applies. PostgreSQL also publishes its changes, through logical replication. Only the tooling changes: for Gilbert, DynamoDB Streams, the change stream of DynamoDB, AWS's key-value database.
4Watertight bulkheads stop a failure from spreading between services
Publishing through the event hub makes each service independent of whoever reads it. The book carries the idea over to failures. A ship is divided into watertight compartments: a leak floods one, not the whole hull. Those partitions are called bulkheads, and the book places two of them, one on each side of the service:
- Outbound bulkhead: the event hub. The service publishes and moves on, without knowing who reads. A downstream failure therefore doesn't block it (lesson 31).
- Inbound bulkhead: the materialized view, a local copy of another service's data, updated by its events. If the source service goes down, the copy still answers with the latest data received (lesson 53).
The counter-example is the direct call. A service that queries another one on every request goes down with it. The book is blunt: "services that have direct dependencies on other services are less resilient" (lesson 53). Its frontend-facing services (BFF, backend for frontend), the ones that serve one specific frontend, "do not wrap other services". They own everything they need to keep running during an outage (lesson 65).
That comfort has a price, which the book names back in the coffee shop: transparency. During an upstream outage, the local copy grows stale. So users must be able to see how old the data they read may be (lesson 29).
5Inverse locking drops stale events instead of retrying
Second family: keeping the system correct when things go wrong, starting with duplicates. The book starts from a plain statement: "exactly-once delivery of messages is unrealistic" (lesson 55). So the same event will sometimes arrive twice. Receiving it twice must change nothing: that is idempotence, like an elevator button you press five times.
The book's example shows where duplicates come from (lesson 41). A program reads events in batches of 100, and event number 60 crashes processing on every attempt. The whole batch then goes round and round, and "the first 59 events will update the target over and over again".
Disorder comes on top of duplicates, and the book handles both with one move. It starts from classic optimistic locking. The row keeps a version number, and if someone wrote in the meantime, your write fails and you retry. Inverse optimistic locking does the opposite: "we drop incoming events if the timestamp is older or equal to the current oplock value in the datastore" (lesson 56).
In practice, each row keeps, in an oplock column (short for optimistic lock), the timestamp of the last event applied. An equal timestamp means a duplicate, an older one means a late event: either way, you ignore it. The book writes it for DynamoDB. In SQL, the same rule boils down to one condition. Here is my translation, not the book's:
INSERT INTO orders (id, status, oplock)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO UPDATE
SET status = EXCLUDED.status, oplock = EXCLUDED.oplock
WHERE orders.oplock < EXCLUDED.oplock;
-- 0 rows touched: duplicate or late event, ignored
ON CONFLICT turns the insert into an update when the order already exists. EXCLUDED is the row you tried to insert, in other words the incoming event. The book's whole rule sits in the final WHERE: you only write if the event is newer.
One last trap remains. A single row may combine several event types, say the order and the driver assignment. With a single lock, a recent order event moves the timestamp forward, and the older assignment is wrongly dropped, fields and all. The fix is one lock per event type, which the book calls an Inverse Oplock Join (lesson 56).
My projections, the read tables kept current by events, already make this move. In my article on idempotence, the upsert only touches the row if the incoming event carries a higher number than the last one applied.
On the write side, I keep one version per aggregate, with a uniqueness constraint on the aggregate and version pair. The book compares timestamps to the millisecond instead. My question, which it doesn't ask: what happens to a second event of the same type, on the same row, within the same millisecond? It would be dropped as a duplicate.
6A control service orchestrates through events and never calls anyone
With duplicates handled, what's left is a process that fails halfway. In the book's food delivery, the order goes to the restaurant, a driver is picked, and every step has its error paths. At first, each service reacts to the others' events. That is choreography: dancers with no leader, adjusting to one another.
As error paths multiply, the logic scatters across every service. The book then finds choreography "harder to understand, trace, debug, test, and maintain" (lesson 83). It gathers that logic into a control service that orchestrates the process. Its definition: "control services react to events by producing more events" (lesson 84).
This conductor never calls another service. It emits an entry event, "task initiated", and waits for the exit event, "task completed" (lesson 87). A participant's contract comes down to that pair of events.
The same contract serves the saga, a long process split into atomic actions, an idea the book traces back to 1987. If a step fails with no way forward, you undo the earlier ones through compensations. Yet, the book warns, "a compensating transaction is not akin to a rollback" (lesson 88). It works like accounting: nothing is erased, mistakes are fixed with adjusting entries.
To know when to compensate, the book tells two failures apart (lesson 89):
- Fault: an unexpected, technical error. You fix it, then resubmit the event to move forward.
- Abort: an expected business condition with no way forward. It triggers the compensations upstream.
The book's example shows an abort, over two steps. The request is submitted, step 1 succeeds, then step 2 aborts. The control service then emits "step 1 cancelled", the compensation for step 1, then "request cancelled". Two rules are enough: any abort cancels the request, and an abort of step 2 also cancels step 1.
In my article on sagas, I argue for choreography by default. I keep a process manager, a handler with its own state, for compensating flows only. Gilbert writes the opposite: "Orchestration is the preferred approach" (lesson 86).
Our positions are closer than they look, since he also advises starting with choreography (lesson 83). The fault versus abort split was missing from my toolkit. It says upfront what you retry and what you compensate.
7A snapshot becomes an event, and for a balance order stops mattering
Compensations repair a process. What remains is computing a state, such as a balance, when events arrive out of order. The book admits that event sourcing "has a bad reputation for being overly complex for most use cases" (lesson 90). The culprit: you have to rebuild state by replaying events.
The usual fix is the snapshot: a picture of the state at one moment, so you only replay what came after. Gilbert moves it. A control service collects deposits and withdrawals, computes the balance, and publishes a "balance" event (lesson 91). The next calculation starts from the latest published balance and adds whatever arrived since.
This calculation copes with disorder thanks to ACID 2.0, four properties borrowed from Pat Helland: associative, commutative, idempotent, distributed. Commutative means order doesn't change the result, like 2 + 3 and 3 + 2. The book's example comes with numbers. In both orders below, each balance in parentheses is a snapshot: the total published after a group of events. The two orders group the events differently:
- First order: deposits of 100 and 30 (balance 130), a withdrawal of 50 and a deposit of 70 (balance 150), a deposit of 20, final balance 170.
- Second order: a deposit of 100 and a withdrawal of 50 (balance 50), deposits of 30 and 20 (balance 100), a deposit of 70, final balance 170.
The book concludes: "the events arrived in different orders and the ending balance of both scenarios is 170" (lesson 91).
The book admits the limit of its example. It follows the happy path and ignores the case of an older event arriving after a snapshot. Gilbert leaves that case to the reader "as a thought exercise" (lesson 91). Yet that is precisely the hard case.
In my event store, a snapshot is an optimization: a state saved every 100 events, which you can throw away without losing anything. For Gilbert, the snapshot is a published event, computed outside the aggregate.
His calculation only works for commutative operations, such as a balance or a counter. A state machine, where "paid" must follow "ordered", needs strict ordering. So I would only carry his snapshots over to balances and counters, with a written answer for the late event.
8Across two regions, replicate the result, not the work
Duplicates and disorder have one last playground: several regions, that is, several of the provider's data centers in different parts of the world. Gilbert lived through the 2017 S3 outage with a single-region system, with nothing he could do (lesson 95). In his experience, a major regional outage happens about every two to three years.
To run in two regions, the temptation is to replicate the event hub: each region receives every event and redoes all the work. Gilbert says no: "I recommend against replicating at the bus level" (lesson 102). Each region would run the same processing, pay twice, and might even get different results.
He replicates the database changes instead, already computed. In his words: "we perform the work once and replicate the output" (lesson 102). A trap follows: the region receiving the copy must not publish the event again. So the book tags each row with its region of origin. An order processed in the East region is copied to the West region. In the West, the function reading the CDC sees the row arrive, reads "East" as its origin, and publishes nothing.
Recovery after an outage relies on the idempotence from idea 5. When a region takes over, it also takes over polling the systems outside the subsystem. It then rewinds about 30 minutes to fill the gaps (lesson 105). Idempotence absorbs the duplicates this creates.
9To erase a person from an immutable lake, destroy their key
Last idea: protecting those facts, kept forever in the lake. The book opens with a true story, Code Spaces in 2014 (lesson 108). An attacker takes the company's cloud account hostage, then deletes everything, backups included, because they lived in the same account. The company goes under.
The book draws a rule from it: each subsystem gets its own cloud account, and backups live in an isolated one. Gilbert also distrusts disk encryption on its own: "Disk-level encryption only secures the data when the disk is removed from the system" (lesson 114). Yet privileged access reads the data in the clear.
So the book encrypts sensitive fields in code, with envelope encryption: a data key encrypts the field, and a master key encrypts the data key. Each event carries, in an eem field (envelope encryption metadata), the encrypted data key and the list of protected fields (lesson 115). A fresh data key is used for every save, so a stolen key opens a single object.
That leaves the GDPR right to erasure, under the EU regulation on personal data. It looks incompatible with a lake where nothing is ever modified. The book's answer is crypto-shredding: one master key per person. When a subject asks for erasure, "we just need to delete the subject-specific key" (lesson 115). Their data stays in the lake, but nobody can decrypt it anymore.
The book's security stops at the edge of the system. The course text never mentions mTLS, where client and server each present a certificate. Nor does it cover zero trust, where no call is taken on faith. Gilbert has a logical reason: his services never call each other, they exchange events.
For a financial system, that isn't always enough. The hub, the lake and outbound calls remain entry points. My article on mTLS covers what the book leaves aside: one TLS port serving three audiences, with different client certificate requirements. On this front, the book doesn't replace a network security policy.
My take, honestly
My verdict, 7.2 out of 10: a good book of mechanisms for event-driven systems, written for AWS, whose principles travel well. It answers a simple question: can you do event sourcing without replaying every aggregate? Yes, with a current state, CDC and an event lake. For anyone who replays aggregates, as in my Go event store, it is the other school, and it holds up.
Its strength: mechanisms that are named, quantified and portable beyond AWS. Inverse locking, the control service that calls no one, replicating the result and the per-person key each boil down to one rule. You can then write that rule in SQL or in Go. Even the coffee shop that hands you two drinks instead of one becomes a design rule. The examples are short and quantified (a coffee shop, a bank account, a batch of 100 events), with few production war stories: hence 7 for Examples.
Its weaknesses, with what they cost you:
- Everything is written for AWS and JavaScript. The database, the streams, the event hub and the functions are all AWS services, plus two of the author's own libraries. Cost: in Go or PHP on PostgreSQL, you translate every example yourself.
- The hard cases stay as exercises. The late event after a snapshot is left to the reader (lesson 91), and millisecond locking is never discussed. Cost: the cases that hurt in production are yours to settle alone.
- Security stops at the edge of the system. No mTLS, no zero trust, as idea 9 notes. Cost: for a financial system, you'll need another source on that front.
- The course version damages the book. "Untitled" lessons, references to missing figures, quizzes with no content, and a few bugs in the listings. Cost: sometimes you have to guess at a figure the lesson describes, hence 7 for Readability.
My advice: read it selectively. Lessons 29 to 36 set out facts and the lake (ideas 1 and 2). Lessons 55, 56 and 61 give idempotence and CDC (ideas 3 and 5). Lessons 83 to 91 cover sagas and snapshots (ideas 6 and 7). Lessons 102 and 115 are enough for ideas 8 and 9. Skim the rest with no regrets, especially away from AWS.
Odilon
Still relevant in 2026?
The principles, yes: they depend on no provider. The edition dates from 2024, and some of the tooling has already aged:
- Event streams go through Highland.js and two of the author's libraries, in JavaScript.
- The deployment pipeline authenticates with a stored key. Today you would rather federate its identity with the cloud through OIDC, keyless.
- The machine learning section predates the rise of large language models.
Outside AWS, you port it idea by idea. The lake can live in any S3-compatible storage. DynamoDB's TTL, a per-row lifetime after which the database deletes the row on its own, needs a scheduled job elsewhere. Hence a 7 out of 10 on the radar's "Aged well" axis.
Who is it for?
Read it if
- You do aggregate-based event sourcing and want to see the other school: current state, CDC and an event lake
- You write event consumers and want precise moves against duplicates and disorder
- You build on AWS Lambda, DynamoDB and EventBridge: the code is written for you
- You need to reconcile an immutable log with the right to erasure
Skip it if
- You are new to CQRS and event sourcing: read Exploring CQRS and Event Sourcing first (the review, 8.2/10)
- You want Go or PHP code ready to copy: it is all JavaScript on AWS
Gilbert says he was doing event sourcing and CQRS without knowing it. He then read Martin Kleppmann's article on turning the database inside out (lesson 50). Read Kleppmann for the why, Gilbert for the how on AWS.
This book is part of ExpertEdge, Packt's subscription for technical books. The code ODILON50 takes 50% off the yearly plan: the book on ExpertEdge. Affiliate link: I earn a commission if you subscribe with this code, and my ratings don't depend on it.
For going further
- The article Event sourcing without an event store: CDC as the log of facts: my PostgreSQL event store and Gilbert's approach, side by side.
- The Exploring CQRS and Event Sourcing review: aggregate-based event sourcing, with process managers and sagas, on a real project.
- The Understanding Eventsourcing review, by Martin Dilger: event sourcing and Event Modeling together.
- The The little Eventmodeling Book review: Dilger's method for drawing an event system before coding it.
- The Designing Data-Intensive Applications review: Kleppmann, the foundation behind this book.
Notes based on the ExpertEdge course version of the 2nd edition: eight of thirteen chapters read, the last one through its two lessons only (polycloud and a quiz). The "My reading" passages and the SQL translation are mine, not the book's.
Source: ExpertEdge course version (Packt 2nd edition, 2024), lessons 1, 29 to 71, 83 to 132, 149 and 150 read in detail.
Comments (0)