🧭 Analogy
A bank statement is event sourcing: it doesn’t store your balance, it stores every deposit and withdrawal, and the balance is computed by replaying them. You can reconstruct what you had on any date and audit every change. But you wouldn’t hand the raw transaction ledger to a partner and ask them to re-derive your balance with your exact rounding rules — that’s the boundary mistake.
What event sourcing is
Most systems use CRUD: a row holds current state and is mutated in place, destroying history. Event sourcing instead records immutable, append-only delta events — the transitions — and rebuilds current state by aggregating them in order. The log of changes is the source of truth; current state is a projection.
graph TD subgraph CRUD["CRUD — mutate in place"] R["row: qty=2"] -->|"update"| R2["row: qty=5 (old gone)"] end subgraph ES["Event sourcing — append deltas"] A["+2"] --> B["+5"] --> C["-3"] --> Sum["replay → qty=4 + full history"] end
The payoff is real: a complete audit trail, the ability to reconstruct any past state (temporal queries), and natural alignment with the durable log. Bellemare’s refrigerator example — modeling additions and removals as deltas and deriving contents — shows it working cleanly within a bounded context.
graph LR D1["itemAddedToCart"] --> AGG["Aggregate / replay"] D2["itemAddedToCart"] --> AGG D3["itemRemovedFromCart"] --> AGG D4["orderPaid"] --> AGG AGG --> STATE["Current state<br/>(a projection)"] AGG -.->|"replay from t0"| PAST["State at any past time"]
The performance lever: snapshots
Replaying a long history on every start is slow. Snapshots checkpoint the derived state periodically (built into Kafka Streams, Flink, and Spark) so a restart resumes from the latest snapshot and replays only the deltas since. This is what makes the Kappa “consume from the beginning” model practical for large data sets.
The key insight
Event sourcing and event-carried state transfer are not competitors — they operate at different scopes. Source deltas inside a context; publish state events across a boundary. The internal ledger is yours; the external contract is everyone’s.
The boundary trap
The data-mesh book is blunt: event sourcing is misused as interdomain communication. The moment a delta event crosses a domain boundary, it becomes part of the public contract. Five reasons it fails for data products:
- Infinite event types whose meaning keeps shifting as the domain evolves.
- Replicated interpretation logic — every consumer must implement the transition rules, which drift out of sync and break under out-of-order delivery.
- Poor mapping to streams — new deltas force notifying every consumer; lumping them into one stream violates the one-evolvable-schema-per-stream convention.
- Inversion of ownership — consumers push business logic into the producer, demanding hyper-specific events like
userReturnedItemAfterTelephoneComplaintthat a single system often can’t even produce because the data spans domains. - No clean history — deltas don’t compact, the log grows unbounded, and the “fix” leads straight back to Lambda.
'Just publish a custom event for me' is a smell
When a consumer asks the producer to emit a delta tailored to their workflow, you’re being pulled into the inversion-of-ownership trap. Publish the immutable state; let the consumer derive whatever transitions and aggregates they need on their side. See event notification vs. state transfer.
See also
- Event notification vs. state transfer — why state events win across boundaries.
- Streams and the log — the substrate for replay and snapshots.
- Schemas and evolution — the contract a delta would freeze in place.
When to use it — and when not
✅ Reach for it when
- You need a complete audit trail and the ability to reconstruct any past state
- Within a single bounded context where you control both writer and reader
- Temporal queries — 'what did this look like at time T?' — are first-class requirements
⛔ Think twice when
- Publishing deltas as an interdomain data product (they become a public contract)
- Consumers would have to replicate your transition-interpretation logic
- You cannot tolerate an unbounded, non-compactable log of fine-grained changes
Related topics
Notification, event-carried state transfer (ECST), state events, and delta events — what each carries, the coupling it implies, and why state events are the right default for shared data.
ed-patternsStreams and the LogThe durable append-only log as the substrate of event-driven systems — partitions, offsets, retention, compaction, tombstones, and the Kappa architecture.
ed-patternsSchemas and EvolutionThe data contract behind every event — explicit schemas, compatibility types (forward, backward, full), the schema registry, and how to handle breaking changes.
Check your understanding
Score: 0 / 41. Event sourcing builds current state by…
State is derived by replaying the ordered deltas. The log of changes is the source of truth; current state is a projection of it.
2. Why does Bellemare say event sourcing is misused as interdomain communication?
Across a boundary you get an infinite, shifting set of event types, replicated logic that drifts, inverted ownership, and a log that won't compact.
3. 'Inversion of ownership' in the delta-events anti-pattern refers to…
Consumers demand events like `userReturnedItemAfterTelephoneComplaint`; the producer ends up responsible for logic and data that may span several domains.
4. A snapshot in an event-sourced system is used to…
Consumer-maintained snapshots (built into Kafka Streams, Flink, Spark) checkpoint state so a restart resumes from the snapshot rather than replaying from offset zero.
Comments
Sign in with GitHub to join the discussion.