Library · Summary & review

Understanding Eventsourcing

By Martin Dilger. What I take from 650 pages on Event Sourcing, Event Modeling and read model patterns.

FR EN
Understanding Eventsourcing book cover, Martin Dilger

Understanding Eventsourcing

Understanding Eventsourcing: Planning and Implementing scalable Systems with Eventmodeling and Eventsourcing

7.5 /10

« Exhaustive, honest about its Java limits, and the first book to cover Event Sourcing and Event Modeling together from A to Z. »

  • AuthorMartin Dilger
  • OriginalNebulit (auto-édité), 2024 · 650 pages
  • NoteEnglish only — no French edition
  • This page~21 min read
Book rating across 5 dimensionsIdeas8/10Practical6/10Readability7/10Aged well8/10Examples7/10

From a price tag in a box to advanced read model patterns: the guide that covers Event Sourcing and Event Modeling together, warts and all.

Why this book

Event Sourcing and Event Modeling are everywhere in architecture talks, job listings, and team discussions. Most resources either give you a three-line definition or dump five hundred lines of Axon boilerplate with no context. This book, written by Martin Dilger — a German consultant who has been coaching teams on Event Modeling since 2021 — does something rarer: it covers both methods together, from whiteboard planning to database read model patterns, and it is honest about what is hard.

Dilger's other book on this site, The little Eventmodeling Book, is a 72-page Q&A guide. Understanding Eventsourcing is his full thesis: 650 pages, with prefaces by Adam Dymitruk (the creator of Event Modeling) and Gabriel Schenker (Head of Platform Engineering at iptiQ). The code is in Kotlin with the Axon framework. The concepts travel. The Java plumbing does not.

The ideas that stick

Ten ideas from the book, in the order of the book's own logic.

1The price tag in the box

Dilger's children are selling toys in the street. Every sale: a price tag (red for the son, yellow for the daughter) dropped into a shoebox. End of day, they empty the box, count tags by color, and each knows their earnings. They never updated a spreadsheet. They naturally invented the Event Store.

That is the analogy Dilger uses in chapter 2, and it is the clearest I have seen for explaining Event Sourcing without jargon. The box is the Event Store: a database you can only append to, never modify or delete. Each tag is an event. An event is a fact that already happened, written in the past tense, permanently immutable. Dilger states the rule plainly: "Whatever happens in the system, you need to record it as an event. If there is no event, it didn't happen." (p. 40)

To know the current balance, you replay the events in order. That is the fundamental difference from CRUD (Create, Read, Update, Delete): CRUD overwrites past state with an UPDATE. Event Sourcing only appends. A shopping cart that starts empty, receives three items, removes one, ends up with two. All three moves are preserved.

Two terms to pin down. An Event Stream is the chronological sequence of all events for one specific entity (a cart, a customer), identified by a Stream-Id — the Event Sourcing equivalent of a primary key. A Projection is a view built by replaying those events into a different format: a SQL table, an Elasticsearch index, even a CSV.

Event Store: append-only stream with replay to projection EVENT STORE · append-only CustomerCreated ItemAdded ×3 ItemRemoved CartSubmitted PaymentConfirmed replay Projection: current state = fold(all events)
The Event Store is append-only. Replaying events in order reconstructs the current state at any point in time. The projection is a derived view — always regenerable from the source of truth.

2Four sticky notes to map any system

Adam Dymitruk writes in his foreword that the root cause of most software failures is not technical: it is the absence of a shared language between developers and the business. Event Modeling is the answer. It is a visual planning method built on four block types, readable by everyone.

The four patterns:

  • State Change (blue Command → orange Event): the only way to modify the system. A user triggers a command, which produces an event if all business rules pass.
  • State View (orange Event → green Read Model): how to read data. An event feeds a view that can be queried.
  • Automation (gear symbol): a background process triggered automatically by an event, a timer, or a user action — for instance, sending a confirmation email after an order.
  • Translation: receiving an external event (third-party API, Kafka topic, CSV file) and converting it into an internal event or Read Model.

What Dilger calls the "completeness check" is the real contribution. Every attribute in a Read Model must be traceable to an Event that already exists in the model. If a "user email" field in a view has no source Event, that data does not yet exist in the system. The rule prevents false assumptions about data availability, one of the most common causes of project delays.

A story from the book: in Vienna, spring 2024, two people argue heatedly about the same feature in a Dymitruk workshop. He pulls up wireframes. The disagreement resolves in five minutes. They were describing two different screens. Dilger estimates that planning should account for sixty to seventy percent of the total project effort. "Commands describe what should happen, events describe what actually happened." (p. 49)

Event Modeling board — all 4 patterns on one e-commerce flow ① State Change ② Automation ③ State View ④ Translation Checkout PlaceOrder OrderPlaced SendEmail Command EmailSent OrderPlaced Order History Stripe API payment_confirmed ⇄ translate PaymentConfirmed Order Status Screen Command Event Read Model Automation Translation
One e-commerce flow, all four patterns. Left to right: State Change (Checkout screen → PlaceOrder command → OrderPlaced event), Automation (OrderPlaced triggers the ⚙ gear → SendEmail command → EmailSent event), State View (OrderPlaced feeds the Order History read model), Translation (Stripe webhook → parallelogram adapter → PaymentConfirmed internal event → Order Status read model). The faded orange box in column ③ is the same OrderPlaced event, referenced by the State View slice.

3CQRS: two ways to talk to the same system

Bertrand Meyer, in the 1980s, formulated the CQS (Command-Query Separation) principle: every method is either a command (it changes state, returns nothing) or a query (it returns data, changes nothing). A method that does both is a source of confusion.

CQRS (Command Query Responsibility Segregation) applies the same idea at the architectural level. You maintain two distinct models: the Write Model (handles commands and event writes) and the Read Model (optimised purely for reading, no business rules).

CQRS and Event Sourcing are independent. They complement each other, but neither requires the other. Dilger gives a concrete example: a PostgreSQL table propagated to Elasticsearch via CDC (Change Data Capture, a tool that detects data-source modifications and propagates them elsewhere). No Event Sourcing in sight — and it is still CQRS.

Eventual consistency is the natural consequence. When the Write Model and Read Model synchronise asynchronously, the view can lag slightly behind reality. Dilger's Amazon example: buy an ebook and immediately check your "Digital Contents" page. The book is not there yet. About thirty seconds. That is visible eventual consistency. The design response: model that delay into the UI rather than pretend it does not exist.

4The anatomy: who does what in the cycle

An event-sourced system is four components passing the baton. The full cycle (chapter 6) in six steps:

  1. A request arrives and is translated into a Command: "AssignContactAddressCommand". A command is an instruction to act.
  2. The Command Handler receives it. It validates structural rules (required fields, postcode format) and delegates to the Aggregate.
  3. The Aggregate is the bouncer — the nightclub doorman who decides what gets in. It checks business rules ("a customer cannot have two identical delivery addresses"). If all rules pass, it writes the resulting event to the Event Store. The Aggregate is the sole authority on data consistency.
  4. The event lands in the Event Store (append-only). It is proof that something changed.
  5. Projectors (Event Handlers) react to the new event. They translate it to SQL tables, search indexes, caches — feeding the Read Models.
  6. To read data, the system issues a query. The Query Handler knows where to look and in what format. It abstracts the storage technology: swapping the read backend does not break clients.
Anatomy of an event-sourced system — 6-step write/read cycle WRITE PATH → ← READ PATH UI Client Command Handler validates structure Aggregate business rules Event Store append-only → new event Projectors event → rows / index Read Model SQL / index / cache Query Handler abstracts storage
The Read Model is disposable. Delete it, replay the Event Store, and it rebuilds itself — the write path never changes. Each new reporting need is just a new Projector listening to the same stream.
// Aggregate — the system's bouncer (Kotlin, concept portable to any OO language)
class CustomerAggregate {
    fun handle(command: AssignContactAddressCommand) {
        // Checks: no duplicate address, at most 2 addresses per customer
        storeEvent(ContactAddressAssigned(...))  // proof it happened
    }
}

5Internal events vs. public contracts

A seductive argument for Event Sourcing: "you have access to all events, so any system can consume your stream." Dilger says plainly that this is wrong. Consuming another service's internal events is technically identical to reading its database directly. Other teams become dependent on your internal event structure. Every refactoring becomes a negotiation.

Dilger draws a clear boundary between two categories:

  • Domain Events : internal facts that capture state changes. They belong to the domain and are free to evolve as rules change.
  • Integration Events (or External Events) : stable contracts published for other systems. They summarise data in a versioned, agreed-upon format — and changing them requires coordination.

An example from the book: the cart's internal events are "Item Added", "Item Removed", "Voucher Applied". A service consuming these must understand all the cart's calculation rules. If those rules change, every consumer breaks. The right approach: publish a "Cart Submitted" event that already contains the calculated total in a clean format. That is the contract.

Event versioning follows two rules:

  • Backwards-compatible change : does not break existing consumers — adding an optional field.
  • Breaking change : does break them — removing or renaming a field. The strategy: version the event (CustomerRegistered_V2) and handle both versions in code until a full migration is done.

6Eight ways to read the same events

Part IV of the book is one of the few places in architecture literature that gives precise names to design decisions most teams make implicitly. Eight read model patterns, each solving a distinct problem:

PatternProblem solvedKey trait
Database Projected Read ModelComplex queries across multiple streamsSQL table, async, rebuilt via replay
Live ModelStrong consistency on one short streamRebuilt from scratch per request, no secondary store
Synchronous ProjectionAsync projection with one consistency-critical pointIn-memory buffer (LinkedBlockingQueue) bridges the lag
Logic Read ModelWhere to put derived calculationsLogic in the Read Model — no side effects, no external calls
SnapshotsPerformance on long streamsState cache at time T — exception, not rule
Processor-TODO-ListAutomations without an explicit SagaRead Model = task list; resulting event = tick the task
Reservation PatternResource uniqueness without distributed ACIDAggregate whose ID is the resource (email, ticket)
Lookup TablesDescriptive data (names, images) not duplicated in eventsID → info projection, kept local to each slice

The Processor-TODO-List is Dilger's most original contribution. Instead of an orchestrated Saga (a pattern for coordinating long-running workflows across services through a sequence of events and compensating actions) with an explicit process definition, a Read Model computes the list of pending tasks. A scheduler checks this list, issues a command per task, and the resulting event ticks it off. Simple, testable, no orchestrator required. As Dilger puts it: "We do not define a specific Process-Orchestrator or Saga-Process-Definition but simply act on the facts in the system. That's it." (ch. 35)

On Snapshots, his position is deliberate: "You have a problem, and you cache it. Now you have two problems." The real fix is to limit stream length through business concepts — the accounting notion of "closing the books" at month-end, for instance. Snapshots are a performance patch, not a design pattern.

7The cost of keeping the past

Event Sourcing keeps the complete history. GDPR (General Data Protection Regulation) requires the right to erasure. The tension is real. Dilger reframes it: the problem is not specific to Event Sourcing. Logs, backups, and Excel exports in CRUD systems have exactly the same problem.

Rule one is a design decision, not a technical one: Data Minimalism. "The simplest data to handle is the data that doesn't exist." (p. 551) Store only what is strictly necessary.

For what must be stored, two techniques:

  • Crypto Shredding: PII (Personally Identifiable Information, such as name, address, email) is encrypted in events with a per-user key stored in a key management service (AWS KMS, HashiCorp Vault). To "forget" a user: delete their key. The events stay in the store but become unreadable. The past is still there, just opaque.
  • Forgettable Payload (a term from Mathias Verraes): personal data is stored in a separate table, referenced by ID in the events. The events contain only the ID. To forget: delete the row in the table. Events remain intact.

After erasure, some projections must be rebuilt. The Event Model is valuable here: it lets you identify exactly which projections use that data and therefore which ones need replaying.

8The Processor-TODO-List: automations without an orchestrator

Every system has automated workflows. After an order confirms, send a confirmation email. After payment, generate the invoice. After a trial expires, deactivate the account. The standard answer is a Saga (a pattern that defines the full workflow as an explicit sequence of steps, with compensating transactions if anything fails) or a Process Manager (an object that routes messages between services and knows which command to issue after each event). Both require a named, maintained definition of the process itself.

Dilger uses neither. His Processor-TODO-List pattern starts from a different question: what does the system already know? The answer is in the Event Store. An order was confirmed. A confirmation email has not yet been sent. A warehouse was not yet notified. Those are not future plans, they are facts, already expressible as events. The gap between "this happened" and "that has not happened yet" is the TODO list. Build a Read Model that computes it.

The mechanics are three steps that repeat. First, events arrive and a Projector writes rows into the TODO List Read Model: one row per pending task, with its status (pending or done) derived from events already in the store. Second, a scheduler, a background job that runs every few seconds, queries the Read Model for pending rows. For each one, it issues a Command. The Command Handler validates and delegates to the Aggregate, which writes the resulting Event. Third, that Event is what the Projector uses to flip the row to "done." The TODO list updates itself from the same stream it is already listening to.

Dilger's own summary of the pattern: "We do not define a specific Process-Orchestrator or Saga-Process-Definition but simply act on the facts in the system. That's it." (ch. 35) The entire workflow state is implicit in the events — no explicit state machine, no orchestrator process to monitor or restart.

Processor-TODO-List: events feed a task list; scheduler reads it and issues commands; resulting events mark tasks done ① events arrive ② TODO list ③ scheduler acts OrderConfirmed ItemShipped projector TODO List — Read Model ⧗ send confirmation email ⧗ notify warehouse ✓ generate invoice reads Scheduler (every 5s) issues SendEmail Command ConfirmationEmailSent marks ✓
The three-step cycle. Events feed the TODO List Read Model (one row per pending task). The Scheduler polls the list and issues a Command per pending row. The resulting Event is what the Projector uses to mark the row done — closing the loop.
// TODO List Read Model: Kotlin concept, portable to any language
// One row per automation task, status derived from events

data class AutomationTask(
    val orderId: String,
    val task: String,           // "send_confirmation_email"
    val status: String          // "pending" | "done"
)

// Projector updates the table on each event:
// OrderConfirmed   → INSERT row (status=pending)
// ConfEmailSent    → UPDATE row (status=done)

// Scheduler: runs every 5 seconds
fun processPending(tasks: List<AutomationTask>) {
    tasks.filter { it.status == "pending" }.forEach { task ->
        commandBus.dispatch(SendConfirmationEmailCommand(task.orderId))
    }
}

What this trades away: the workflow is implicit. There is no single file that reads "if order confirmed, send email, then notify warehouse." The logic is distributed across Projectors and Handlers. For complex flows with explicit failure modes and compensating transactions, a Saga is still the right tool. Dilger openly prefers this pattern — and explicitly admits he may not even be using the term "Saga" correctly — but the choice depends on how much the team needs the workflow visible in one place.

9The Reservation Pattern: uniqueness without distributed transactions

Some business rules require uniqueness across the entire system. An email address must be registered only once. A concert seat can be booked by only one person. A coupon code can be used only once. In a standard SQL database, you enforce this with a unique index: the database rejects any duplicate insert at the column level. But in an event-sourced system, where events go into an append-only store and projections are eventually consistent, there is no obvious equivalent. Two users can simultaneously try to book the same seat, both read "available" from the projection (which may be slightly behind), both emit a BookingConfirmed event, and the seat is double-booked before anyone notices.

The Reservation Pattern solves this by making the Aggregate ID the resource itself. Instead of an aggregate for "concert" with a list of seats, you create an Aggregate whose ID is the specific resource: seat-A15-concert-2024-06-20. The Event Store uses optimistic concurrency: every write specifies the expected stream version. If two users try to create the same Aggregate simultaneously, both start at version 0. One write succeeds, version becomes 1. The second write, also expecting version 0, fails with a version conflict. The first one to write wins. The Event Store becomes the distributed lock.

// Reservation Pattern — the Aggregate ID IS the resource
// Two concurrent booking attempts for the same seat:

// User A                          User B
// read stream: version=0          read stream: version=0
// (seat appears available)        (seat appears available)
//
// write SeatBooked                write SeatBooked
//   expectedVersion: 0              expectedVersion: 0
//   → OK, version becomes 1         → CONFLICT: expected 0, found 1
//
// User A: seat confirmed          User B: "seat unavailable" response
//   (SeatBooked event written)      (optimistic lock exception caught,
//                                    translate to business error)

What makes this clean: no two-phase commit, no distributed coordinator, no saga to manage the lock timeout. The uniqueness guarantee lives in the Event Store's built-in version check, which every production Event Store implements (EventStoreDB, Marten, Axon all support expected-version writes). The trade-off is that each unique resource becomes its own Aggregate, which can lead to a very large number of small streams — fine in practice, but worth knowing before applying it at scale.

Dilger extends the pattern to the two-phase reservation problem: reserve first, confirm later. A ticket platform may want to hold a seat for two minutes while the user completes payment. This is two commands: ReserveSeat produces SeatReserved (held for 2 min); ConfirmSeat produces SeatConfirmed. The Processor-TODO-List from idea 8 handles the timeout: a scheduler finds reservations whose SeatReserved event is older than two minutes with no matching SeatConfirmed event, and issues a ReleaseSeat command. Two patterns composing cleanly.

10Testing an event-sourced system: Given-When-Then

Event Sourcing has a hidden testing gift that most introductions never mention. In a CRUD system, testing business logic means setting up database state, calling a method, and querying the database again to assert the result. The test depends on schema migrations, database connections, seed scripts. Any of those can go wrong before you even reach the code you wanted to test. Dilger shows a different approach: Aggregate tests are pure functions.

The Aggregate only needs two things to run: past events (to rebuild its state) and a new Command. It produces Events. Nothing else. No database connection, no HTTP call, no external dependency. The test structure mirrors Event Modeling's own slice notation — Given-When-Then — and reads exactly like a specification:

// Given-When-Then test for a Cart Aggregate
// No mocks, no database, no Spring context

@Test
fun `adding an item to a submitted cart is rejected`() {
    // GIVEN: past events that reconstruct the aggregate state
    val history = listOf(
        CartCreated(cartId = "cart-42"),
        ItemAdded(cartId = "cart-42", itemId = "book-1", qty = 1),
        CartSubmitted(cartId = "cart-42")
    )

    // WHEN: a new command is issued
    val aggregate = CartAggregate.rehydrate(history)
    val result = aggregate.handle(AddItemCommand(cartId = "cart-42", itemId = "book-2"))

    // THEN: the result is a business error, not a new event
    assertThat(result).isInstanceOf(CartAlreadySubmittedError::class.java)
}

@Test
fun `submitting a cart produces CartSubmitted and clears the pending flag`() {
    val history = listOf(
        CartCreated(cartId = "cart-42"),
        ItemAdded(cartId = "cart-42", itemId = "book-1", qty = 2)
    )
    val aggregate = CartAggregate.rehydrate(history)
    val events = aggregate.handle(SubmitCartCommand(cartId = "cart-42"))

    assertThat(events).containsExactly(
        CartSubmitted(cartId = "cart-42", totalItems = 2)
    )
}

Each test is a self-contained story. The Given section is an explicit list of past facts — the equivalent of a database fixture, but readable, typed, and version-controlled alongside the test. The When is the command under test. The Then is the list of Events produced (or the business error thrown). No hidden state, no shared setup, no cleanup. Rehydrating from the history list is the same replay the production system uses; the test exercises exactly the same path as the real runtime.

Dilger also shows Read Model tests, which follow the same pattern but assert the resulting projection state: Given [list of events], Then [this is what the Read Model table looks like]. Those tests verify that Projectors correctly translate events to query-optimised rows. Together, the two test types give full coverage of an event-sourced feature — the write side (Aggregate + Command Handler) and the read side (Projector + Read Model) — without a live database or running server.

Three things I did not know

My honest take

I am a PHP/JS web developer, not a Java/Spring developer. This book has two faces. The first half — Foundations and Modeling — is among the clearest writing I have read on Event Sourcing and Event Modeling. The shoebox analogy, the completeness check, the internal versus external event boundary: I wish I had this five years ago when CQRS came up in meetings and I had no idea what anyone was talking about. Part IV's pattern catalog is also genuinely useful — named, bounded, justified.

The second half (Part III, the Axon implementation) is a different planet. Kotlin with Axon's QueryGateway, HandlerEnhancerDefinition, and @EventHandler annotations: readable, but it assumes a Spring/Java ecosystem most web developers do not have. Six hundred and fifty pages for both audiences together is an investment.

What travels: the eight read model patterns, the internal-versus-integration-event boundary, the Processor-TODO-List, the Reservation Pattern, the Given-When-Then test structure, the completeness check. None of those depend on Java. Pair this book with Exploring CQRS and Event Sourcing (the Microsoft patterns & practices guide) for the implementation-level view of the same ideas, then decide which stack fits your context.

Odilon

Still relevant in 2026?

Yes — and more than the 2012 Microsoft guide. The Foundations section will age slowly; the patterns and the Event Modeling method even slower. The Axon-specific implementation will date faster, but the patterns it illustrates are portable. One angle has grown since the book was written: AI agents now consume the same event streams as humans, with the same need for well-named, self-describing events. A clean event language is as valuable to an LLM generating code as to a developer reading a stream.

For whom?

Read it if

  • You are joining a team that speaks CQRS/ES/Event Modeling and you have not yet decoded the vocabulary
  • You are planning a distributed system and want a collaborative modeling method
  • You work in Java/Spring and are considering Event Sourcing
  • You know Event Sourcing conceptually but want named read model patterns (the eight in Part IV)

Skip it if

  • You want a framework-agnostic ES guide — the examples are too Axon-specific
  • You need complete event schema versioning coverage — the book points you to Greg Young's Leanpub book instead
  • Six hundred and fifty pages is too much — chapters 1–9 stand alone at about 150 pages

Going further

For the conceptual foundation of Event Sourcing patterns in a DDD (Domain-Driven Design) context, Exploring CQRS and Event Sourcing (Microsoft patterns & practices) follows a real project through its iterations, including the parts that went wrong. For Event Modeling specifically, The little Eventmodeling Book (also Dilger) is a 72-page Q&A companion that covers the method in isolation. For event schema versioning — the one hard problem this book leaves open — Greg Young's Versioning in an Event Sourced System (Leanpub, 2017) is the only dedicated treatment.

Comments (0)