Event Sourcing
Purpose
Decide, and then correctly operate, a system whose durable truth is an append-only sequence of
facts rather than a mutable row. State is a fold over that sequence; the row you used to
update becomes a derived view you can throw away and rebuild.
This can preserve a sequenced decision history, reconstruct state under a specified model and
build new views from retained facts. It does not automatically make history complete,
tamper-evident, legally immutable or semantically reproducible: missing external inputs,
changed projection code and retention/redaction policies still matter. The durable obligation
is to keep event meaning, replay tooling and governance compatible for the declared horizon.
Compare audit/history storage before adopting event sourcing solely for auditability;
measure actual operating cost rather than assuming a fixed ratio. Design schema
evolution, projection rebuilds and retention/erasure before committing long-lived history.
Inspect the target JDK, event-store/server and client versions, append/subscription contracts,
retention and transaction manager. Java examples use Java 21 standard pattern switches
and are partial domain examples, not client SDK implementations. Do not upgrade a project
to use them. With missing evidence, keep adoption/correctness claims conditional and name
the store-level test needed. Return the driver, alternative, authoritative boundary,
recovery/visibility contract and validation evidence.
Workflow
- State the driver, and check it against the cheaper alternative. Audit? A history
table. Debugging? Structured logs. Temporal queries? Bitemporal columns. When the
business genuinely reasons in events — ledgers, trading, workflow, anything where the
sequence is the domain — does the cost become proportionate.
- Fix the stream boundary. It is usually one aggregate's ordering/concurrency unit. Model
maximum length, contention, invariant scope and migration strategy; changing it later is a
data migration, not literally impossible.
- Design events as facts, in past tense, in the business's language.
FundsWithdrawn,
not BalanceUpdated. An event named after a data change is a row update wearing a hat.
- Decide concurrency before writing code. Appends carry an expected version; a mismatch
is a conflict the caller must resolve. This is optimistic locking with a different name
(
offline-concurrency-control).
- Design reads and consistency explicitly. Separate asynchronous projections are common
and stale by lag; inline projections, direct stream reads or same-transaction read models
have different semantics. CQRS deployment is optional, not implied by event sourcing.
- Write the rebuild before you need it. A projection you cannot rebuild is a database you
cannot migrate. Time it against production volume now, not during the incident.
- Answer the three hard questions up front — event versioning, personal-data erasure, and
what the user sees immediately after they save. Each is cheap now and expensive later.
What changes when the log is the truth
Command ──► Aggregate ──► Event(s) ──► append to stream
▲ │ (expected version)
│ │
fold over the ▼
stream to rebuild ┌─────────────┐
current state │ EVENT STORE │ ← the only truth
└──────┬──────┘
│ subscribe
┌────────────┼────────────┐
▼ ▼ ▼
projection projection projection
(SQL view) (search) (report)
│ │ │
└──── asynchronous example ────────┘
rebuildable from retained facts
Three consequences follow, and they are the whole of the trade:
- Normal business correction appends a new fact. Exceptional redaction or repair needs
the controlled migration and retention contract described below.
- Queries need a state representation. It may be a direct fold, snapshot or projection;
only asynchronously maintained projections are necessarily stale.
- The write model and the read model diverge on purpose. That divergence is what makes
both simple; it is also the divergence a team must be willing to operate.
Decision rules
The driver is "we need an audit trail"
→ compare append-only history and temporal tables against required
actor, retention and tamper-evidence controls; replay is a separate need.
The driver is "we might want to analyse this later"
→ do not event source. Emit events to a log or warehouse
alongside a normal database (event-driven-architecture).
The business itself reasons in immutable facts — ledger entries,
trades, claim events, workflow transitions — and the sequence carries
meaning current state cannot express
→ the strongest case. Adopt it for those aggregates.
Requirements demand "what did it look like on date X" as a first-class
query, not a report
→ a real case, but compare against bitemporal tables first;
they are far cheaper if the need is only historical read.
The domain is CRUD, and the state is the truth the business cares about
→ do not event source. You are buying a rebuild pipeline and a
versioning problem to store a form (domain-logic-organization).
Event sourcing is proposed for the WHOLE system
→ almost certainly wrong. Apply it per aggregate. Most systems
have one or two aggregates that deserve it and many that do not.
Personal data has erasure or retention obligations
→ resolve with privacy/legal owners before adopting. Minimise data;
redaction, segregated mutable data or cryptographic deletion each
has assumptions and derived-copy obligations.
The stream for one aggregate will grow without bound (a device feed,
a long-lived account)
→ use indexed incremental reads and measure replay; snapshot, close
on a business boundary, or redesign when the SLO requires it.
Rules
- Events record committed facts. Validate commands before append; replay must not
reapply today's command rules to yesterday's accepted decisions. Still reject/quarantine
corrupt, unknown or structurally invalid history rather than silently inventing state.
- The aggregate decides; the event records. Load the stream, fold it into state, validate the
command against that state, emit events. This is a functional core with an append at the end
(
humble-objects-and-functional-core).
- The append is the transaction boundary. All events from one command are appended
atomically at an expected version, or none are. A conflicting version means someone else
wrote first — surface it as a conflict, do not blind-retry a command whose decision was made
against stale state (
enterprise-transactions).
- Carry a command id and record it in the event's metadata. The version check catches a
concurrent writer; it cannot catch a retry after an unknown outcome, which re-decides and
appends a second legitimately-versioned copy of the same command. Expected version is
concurrency control, not idempotency (
idempotency).
- Treat committed events as immutable in normal business flow and correct with new facts.
Exceptional redaction/repair may be legally or operationally required; use a controlled,
auditable stream-rewrite/version migration and rebuild every dependent projection. Append-
only storage alone is not tamper evidence.
- For a transactional projection, apply the fold and advance its checkpoint atomically,
using a conditional advance or another proven ownership/deduplication protocol. A failed
advance can mean a missing predecessor, not just a duplicate; do not acknowledge it blindly.
Two workers reading the same
watermark both apply a non-idempotent fold; this is the check-then-act that
idempotency
forbids (delivery-semantics).
- Rebuild time is a capacity metric. Measure it as history grows, and know the number
before an incident forces a rebuild. When it exceeds the acceptable window the answers are
snapshots, a parallel rebuild with a cut-over, or a carry-forward event that bounds the
stream — never simply truncating history, which makes the snapshot the source of truth.
- In classic event sourcing, snapshots are rebuildable optimization: deleting them must still
permit correct replay from retained events. If a design compacts/deletes the prefix and makes
a checkpoint authoritative, name that different retention/audit contract explicitly.
- Read-your-own-writes is the user-visible cost, and it is decided per screen rather than
per system. The mitigations differ in what they cost and what they can actually deliver
(
references/projections-and-evolution.md, consistency-models).
- Event schema evolution has no free option. Format-compatible additive changes are often
cheapest, but legality depends on format, defaults and compatibility mode. Alternatives are
upcasting, parallel event types, migration or a bounded legacy reader over the retention
horizon (
schema-evolution-and-compatibility).
- Publish integration events from a durable subscription/CDC or an explicitly atomic append-
and-publish mechanism. An ordinary append followed by broker send in the command handler is
a dual write. Treat the event log as the source for a replayable relay, while translating
internal events at the boundary (
distributed-transactions-and-sagas).
- Internal events are not integration events. Publishing an aggregate's internal events to
other services couples them to your write model's evolution. Translate at the boundary
(
event-driven-architecture, rpc-and-api-contracts).
- Auditability needs more than append-only APIs: authorize appends per stream/tenant, protect
administrator mutation paths, record actor/causation, define hash/signature or WORM controls
when tamper evidence is required, and test backup restore plus projection reconciliation.
References
KurrentDB Java client: expected-state and atomic append
Apache Kafka log and retention design
GDPR Article 17 — right to erasure
Deciding and designing — the honest comparison
against audit tables, temporal tables and change-data-capture; choosing stream boundaries and
bounding unbounded ones; designing event payloads; the expected-version concurrency model
worked through in Java, including the unknown-outcome case that duplicates a command;
choosing the store and the payload format; snapshots. Read when deciding whether to adopt, or
designing the write side.
Projections, evolution and erasure — the atomic
position advance and why a read-then-skip corrupts a fold, gapless-feed assumptions, rebuild
and cut-over strategies, the five read-your-own-write mitigations with what each actually
delivers, event versioning through upcasting and copy-and-replace, and what crypto-shredding
does and does not erase. Read when building the read side or facing an event that must
change.
1---2name: event-sourcing3description: Event streams as authoritative state: adoption criteria, stream boundaries, expected-version appends, command idempotency, snapshots, projection correctness/rebuild, temporal replay, schema evolution and erasure. Use when event sourcing is proposed, projections drift, old payloads must evolve, or write/read visibility surprises users. Integration messaging, delivery, sagas, mutable-row locking and replica consistency remain separate skills.4---56# Event Sourcing78## Purpose910Decide, and then correctly operate, a system whose durable truth is an append-only sequence of11facts rather than a mutable row. State is a fold over that sequence; the row you used to12update becomes a derived view you can throw away and rebuild.1314This can preserve a sequenced decision history, reconstruct state under a specified model and15build new views from retained facts. It does **not** automatically make history complete,16tamper-evident, legally immutable or semantically reproducible: missing external inputs,17changed projection code and retention/redaction policies still matter. The durable obligation18is to keep event meaning, replay tooling and governance compatible for the declared horizon.1920Compare audit/history storage before adopting event sourcing solely for auditability;21measure actual operating cost rather than assuming a fixed ratio. Design schema22evolution, projection rebuilds and retention/erasure before committing long-lived history.2324Inspect the target JDK, event-store/server and client versions, append/subscription contracts,25retention and transaction manager. Java examples use Java 21 standard pattern switches26and are partial domain examples, not client SDK implementations. Do not upgrade a project27to use them. With missing evidence, keep adoption/correctness claims conditional and name28the store-level test needed. Return the driver, alternative, authoritative boundary,29recovery/visibility contract and validation evidence.3031## Workflow32331. **State the driver, and check it against the cheaper alternative.** Audit? A history34 table. Debugging? Structured logs. Temporal queries? Bitemporal columns. When the35 business genuinely reasons in events — ledgers, trading, workflow, anything where the36 sequence _is_ the domain — does the cost become proportionate.372. **Fix the stream boundary.** It is usually one aggregate's ordering/concurrency unit. Model38 maximum length, contention, invariant scope and migration strategy; changing it later is a39 data migration, not literally impossible.403. **Design events as facts, in past tense, in the business's language.** `FundsWithdrawn`,41 not `BalanceUpdated`. An event named after a data change is a row update wearing a hat.424. **Decide concurrency before writing code.** Appends carry an expected version; a mismatch43 is a conflict the caller must resolve. This is optimistic locking with a different name44 (`offline-concurrency-control`).455. **Design reads and consistency explicitly.** Separate asynchronous projections are common46 and stale by lag; inline projections, direct stream reads or same-transaction read models47 have different semantics. CQRS deployment is optional, not implied by event sourcing.486. **Write the rebuild before you need it.** A projection you cannot rebuild is a database you49 cannot migrate. Time it against production volume now, not during the incident.507. **Answer the three hard questions up front** — event versioning, personal-data erasure, and51 what the user sees immediately after they save. Each is cheap now and expensive later.5253## What changes when the log is the truth5455```text56 Command ──► Aggregate ──► Event(s) ──► append to stream57 ▲ │ (expected version)58 │ │59 fold over the ▼60 stream to rebuild ┌─────────────┐61 current state │ EVENT STORE │ ← the only truth62 └──────┬──────┘63 │ subscribe64 ┌────────────┼────────────┐65 ▼ ▼ ▼66 projection projection projection67 (SQL view) (search) (report)68 │ │ │69 └──── asynchronous example ────────┘70 rebuildable from retained facts71```7273Three consequences follow, and they are the whole of the trade:7475- **Normal business correction appends a new fact.** Exceptional redaction or repair needs76 the controlled migration and retention contract described below.77- **Queries need a state representation.** It may be a direct fold, snapshot or projection;78 only asynchronously maintained projections are necessarily stale.79- **The write model and the read model diverge on purpose.** That divergence is what makes80 both simple; it is also the divergence a team must be willing to operate.8182## Decision rules8384```text85The driver is "we need an audit trail"86 → compare append-only history and temporal tables against required87 actor, retention and tamper-evidence controls; replay is a separate need.8889The driver is "we might want to analyse this later"90 → do not event source. Emit events to a log or warehouse91 alongside a normal database (event-driven-architecture).9293The business itself reasons in immutable facts — ledger entries,94trades, claim events, workflow transitions — and the sequence carries95meaning current state cannot express96 → the strongest case. Adopt it for those aggregates.9798Requirements demand "what did it look like on date X" as a first-class99query, not a report100 → a real case, but compare against bitemporal tables first;101 they are far cheaper if the need is only historical read.102103The domain is CRUD, and the state is the truth the business cares about104 → do not event source. You are buying a rebuild pipeline and a105 versioning problem to store a form (domain-logic-organization).106107Event sourcing is proposed for the WHOLE system108 → almost certainly wrong. Apply it per aggregate. Most systems109 have one or two aggregates that deserve it and many that do not.110111Personal data has erasure or retention obligations112 → resolve with privacy/legal owners before adopting. Minimise data;113 redaction, segregated mutable data or cryptographic deletion each114 has assumptions and derived-copy obligations.115116The stream for one aggregate will grow without bound (a device feed,117a long-lived account)118 → use indexed incremental reads and measure replay; snapshot, close119 on a business boundary, or redesign when the SLO requires it.120```121122## Rules123124- **Events record committed facts.** Validate commands before append; replay must not125 reapply today's command rules to yesterday's accepted decisions. Still reject/quarantine126 corrupt, unknown or structurally invalid history rather than silently inventing state.127- The aggregate decides; the event records. Load the stream, fold it into state, validate the128 command against that state, emit events. This is a functional core with an append at the end129 (`humble-objects-and-functional-core`).130- **The append is the transaction boundary.** All events from one command are appended131 atomically at an expected version, or none are. A conflicting version means someone else132 wrote first — surface it as a conflict, do not blind-retry a command whose decision was made133 against stale state (`enterprise-transactions`).134- **Carry a command id and record it in the event's metadata.** The version check catches a135 concurrent writer; it cannot catch a retry after an _unknown_ outcome, which re-decides and136 appends a second legitimately-versioned copy of the same command. Expected version is137 concurrency control, not idempotency (`idempotency`).138- Treat committed events as immutable in normal business flow and correct with new facts.139 Exceptional redaction/repair may be legally or operationally required; use a controlled,140 auditable stream-rewrite/version migration and rebuild every dependent projection. Append-141 only storage alone is not tamper evidence.142- For a transactional projection, apply the fold and advance its checkpoint atomically,143 using a conditional advance or another proven ownership/deduplication protocol. A failed144 advance can mean a missing predecessor, not just a duplicate; do not acknowledge it blindly.145 Two workers reading the same146 watermark both apply a non-idempotent fold; this is the check-then-act that `idempotency`147 forbids (`delivery-semantics`).148- **Rebuild time is a capacity metric.** Measure it as history grows, and know the number149 before an incident forces a rebuild. When it exceeds the acceptable window the answers are150 snapshots, a parallel rebuild with a cut-over, or a carry-forward event that bounds the151 stream — never simply truncating history, which makes the snapshot the source of truth.152- In classic event sourcing, snapshots are rebuildable optimization: deleting them must still153 permit correct replay from retained events. If a design compacts/deletes the prefix and makes154 a checkpoint authoritative, name that different retention/audit contract explicitly.155- **Read-your-own-writes is the user-visible cost**, and it is decided per screen rather than156 per system. The mitigations differ in what they cost and what they can actually deliver157 (`references/projections-and-evolution.md`, `consistency-models`).158- **Event schema evolution has no free option.** Format-compatible additive changes are often159 cheapest, but legality depends on format, defaults and compatibility mode. Alternatives are160 upcasting, parallel event types, migration or a bounded legacy reader over the retention161 horizon (`schema-evolution-and-compatibility`).162- **Publish integration events from a durable subscription/CDC or an explicitly atomic append-163 and-publish mechanism.** An ordinary append followed by broker send in the command handler is164 a dual write. Treat the event log as the source for a replayable relay, while translating165 internal events at the boundary (`distributed-transactions-and-sagas`).166- **Internal events are not integration events.** Publishing an aggregate's internal events to167 other services couples them to your write model's evolution. Translate at the boundary168 (`event-driven-architecture`, `rpc-and-api-contracts`).169- Auditability needs more than append-only APIs: authorize appends per stream/tenant, protect170 administrator mutation paths, record actor/causation, define hash/signature or WORM controls171 when tamper evidence is required, and test backup restore plus projection reconciliation.172173## References174175- [KurrentDB Java client: expected-state and atomic append](https://docs.kurrent.io/clients/java/v1.2/appending-events)176- [Apache Kafka log and retention design](https://kafka.apache.org/41/implementation/log/)177- [GDPR Article 17 — right to erasure](https://eur-lex.europa.eu/eli/reg/2016/679/art_17/oj)178179- [Deciding and designing](references/deciding-and-designing.md) — the honest comparison180 against audit tables, temporal tables and change-data-capture; choosing stream boundaries and181 bounding unbounded ones; designing event payloads; the expected-version concurrency model182 worked through in Java, including the unknown-outcome case that duplicates a command;183 choosing the store and the payload format; snapshots. Read when deciding whether to adopt, or184 designing the write side.185- [Projections, evolution and erasure](references/projections-and-evolution.md) — the atomic186 position advance and why a read-then-skip corrupts a fold, gapless-feed assumptions, rebuild187 and cut-over strategies, the five read-your-own-write mitigations with what each actually188 delivers, event versioning through upcasting and copy-and-replace, and what crypto-shredding189 does and does not erase. Read when building the read side or facing an event that must190 change.