🧭 Analogy
A bank account isn’t a single “balance” cell that gets overwritten — it’s a ledger of every deposit and withdrawal, and the balance is computed from the list. Event sourcing applies that idea to any aggregate: keep the full story, derive the current state.
The dimension of time
An event-sourced domain model is the same domain model from entities and aggregates — same value objects, same aggregates, same domain services — with one difference: how aggregate state is persisted. Instead of storing the current state (a state-based model, which loses history), the system stores domain events describing every change and treats them as the source of truth. Every state change must be a domain event.
graph TD subgraph SB["State-based — overwrite (history lost)"] R1["balance = 100"] -->|"deposit 50"| R2["balance = 150 (old value gone)"] end subgraph ES["Event-sourced — append (full history)"] E1["Opened"] --> E2["Deposited 100"] --> E3["Withdrew 30"] --> E4["Deposited 50"] E4 --> Bal["balance = 120 (computed)"] end
graph LR
Cmd["Command"] --> Load["1. Load events"]
Load --> Recon["2. Reconstitute state<br/>(replay / project)"]
Recon --> Exec["3. Execute command<br/>(produce NEW events)"]
Exec --> Commit["4. Append events<br/>(expectedVersion)"]
Commit --> Store[("Append-only event store")]
Store --> Proj1["Projection: state"]
Store --> Proj2["Projection: search"]
Store --> Proj3["Projection: analytics"]How it works
The operation script is: load the aggregate’s events; reconstitute state by projecting them; execute the command, producing new events; commit the new events. Commands don’t set flags directly — RequestEscalation creates a TicketEscalated event, and appending it dynamically dispatches state.Apply(event).
- Event store — The append-only database. It must not allow modifying or deleting events (except migrations). Minimum operations: fetch all events for an entity, and append events.
Appendtakes anexpectedVersionfor optimistic concurrency. - Projection — Transformation logic that sequentially applies events to build a state representation. You are not limited to one: over the same lead events you can build a
LeadStateProjection(the current row), aLeadSearchModelProjection(accumulating historical values so agents search by old details), and anAnalysisModelProjection(counts for BI). New projections can be added anytime. - Version field — A counter incremented per applied event; apply the first N events to get the state at version N — time travel.
What you gain
Time travel (reconstitute any past state for analysis, optimization, retroactive debugging); deep insight (add new projections over existing history anytime); a strongly-consistent audit log (legally useful, ideal for money); and advanced optimistic concurrency (inspect exactly which events conflicted).
Don't fake event sourcing
Logfiles are eventually inconsistent; hand-written log tables get forgotten by future engineers; DB-trigger history tables capture only dry facts and lose the business why. None of these gives the strong-consistency guarantee of a real event store. Also don’t reach for event sourcing reflexively — let the domain’s needs (money, audit, deep analysis) drive it.
Performance and operations
- Snapshot — Cache a continuously-updated projection; load the snapshot plus only newer events. It’s an optimization that must be justified — typically unnecessary below ~10,000 events per aggregate (most average under 100). Premature snapshots are accidental complexity, and needing them may signal too-large boundaries.
- Sharding the event store by aggregate ID (all of one instance’s events together) makes the model easy to scale.
- Forgettable payload pattern — For GDPR-style deletion in an append-only store: store sensitive data encrypted, keep the key in external storage (key = aggregate ID); deleting the key makes the data permanently unreadable.
Event sourcing pairs with CQRS
Because the event store is optimized for appending and replaying — not querying — event-sourced models rely on CQRS to project events into queryable read models. The two patterns go together.
See also
- Entities and aggregates — the model whose state is sourced from events.
- Domain events and services — the events being stored.
- CQRS — projecting events into queryable read models.
When to use it — and when not
✅ Reach for it when
- For money, audit, and deep-analysis subdomains where full history and a legally useful audit log matter.
- When you want to add new task-optimized read models over the same history later.
- When time travel and retroactive debugging would provide real value.
⛔ Think twice when
- When the domain has no need for history — a state-based domain model is simpler.
- Don't fake it with logfiles, hand-written log tables, or DB-trigger history tables.
- Don't add snapshots prematurely — most aggregates have under 100 events.
Related topics
Entities have identity; aggregates are the consistency and transactional boundary mutated only through their root and committed atomically.
ddd-tacticalDomain Events and Domain ServicesPast-tense messages that announce what happened, and stateless objects that host logic spanning multiple aggregates.
ddd-tacticalCQRSCommand-Query Responsibility Segregation — one strongly-consistent command model and any number of read-only projections.
Check your understanding
Score: 0 / 41. In event sourcing, what is the source of truth?
Every state change is represented and persisted as a domain event in an append-only event store; those events are the source of truth, and current state is reconstituted by replaying them.
2. What is a projection?
A projection applies events sequentially (overloaded Apply methods) to build a state representation; you can add new projections later over the existing events.
3. How does the event store enforce concurrency on append?
Append takes an expectedVersion; on mismatch you can inspect exactly which events were concurrently appended and decide whether they truly conflict — advanced optimistic concurrency.
4. Which is a real advantage of event sourcing?
Benefits include time travel, deep insight (new projections anytime), a strongly-consistent audit log, and advanced concurrency — at the cost of a learning curve and harder schema evolution.
Comments
Sign in with GitHub to join the discussion.