Event Sourcing
Event Sourcing persists state as a sequence of state-changing events — not current row snapshot.
Introduction
Event Sourcing persists state as a sequence of state-changing events — not current row snapshot. Reconstruct any point in time by replaying events. Current state is a fold over history. Audit, debugging, and temporal queries become first-class.
The story
A bank audit asked: "show every state change for account #12345 in 2023." With Event Sourcing — one query against the event log. Competitors with snapshot DBs spent months reconstructing history from scattered logs.
The business problem
Snapshot-only storage loses history and blocks temporal queries:
- Audit failure: can't prove who changed what when.
- Debug blind: production bug needs past state reconstruction.
- Temporal queries: "balance on March 1" impossible from current row.
- Analytics gap: analysts want event stream, not just latest state.
The problem teams faced
Event Sourcing fits when:
- Complete audit trail is regulatory or business requirement.
- Temporal queries and replay are core features.
- Debugging requires reconstructing historical state.
- Event-driven analytics and projections are first-class consumers.
Understanding the topic
Intent: Store state changes as events; rebuild state by replay.
- Event store — append-only log of domain events.
- Aggregate — applies events to rebuild state; emits new events on command.
- Snapshot — optional periodic state cache to speed replay.
- Projection — read models built from event stream (pairs with CQRS).
Internal architecture
Event-sourced account aggregate:
class Account {private balance = 0private changes: DomainEvent[] = []static fromHistory(events: DomainEvent[]) {const a = new Account()events.forEach(e => a.apply(e, record: false))return a}deposit(amount: Money) {this.apply(new MoneyDeposited(amount), record: true)}private apply(event: DomainEvent, record: boolean) {if (event instanceof MoneyDeposited) this.balance += event.amountif (record) this.changes.push(event)}uncommittedEvents() { return this.changes }}
Visual explanation
Three diagrams cover event log, replay, and vs snapshot DB:
Informative example
Before / after — snapshot update to event append:
// ❌ Snapshot only — history lostawait db.query("UPDATE accounts SET balance = $1 WHERE id = $2", [newBalance, id])// ✅ Event sourced — append event, rebuild stateconst events = await eventStore.load(id)const account = Account.fromHistory(events)account.withdraw(amount) // produces MoneyWithdrawn eventawait eventStore.append(id, account.uncommittedEvents(), expectedVersion: events.length)// Audit: SELECT * FROM events WHERE stream_id = id AND ts BETWEEN ...
Execution workflow
Load history
Fetch events for aggregate ID from event store.
Real-world use
EventStoreDB, Axon Framework, banking ledger systems, Kafka as event log (with caveats), Marten (.NET), event-sourced DDD aggregates.
Production case study
Bank audit trail:
- Requirement: complete 2023 state change history per account.
- Event Sourced: single stream query — minutes.
- Competitors: months reconstructing from partial logs.
Trade-offs
- Pro: perfect audit; temporal queries; natural event-driven projections.
- Con: complexity; event schema evolution; replay performance without snapshots.
- Con: not every domain needs full history — YAGNI cost.
Decision framework
- Use when audit/temporal/replay are core requirements.
- Use with CQRS for read projections from event stream.
- Avoid when simple CRUD with no history need — massive overhead.
- Plan event versioning/upcasting from day one.
Best practices
- Events immutable — never edit; compensating events for corrections.
- Optimistic concurrency on stream version — detect conflicts.
- Snapshots for long streams — replay performance.
- Outbox for reliable downstream publish after append.
Anti-patterns to avoid
- Event sourcing entire app including simple config tables.
- Mutable events or deleting from log — breaks audit.
- No upcasting strategy — schema change breaks replay.
Common mistakes
- Long replay without snapshots — slow aggregate load.
- GDPR delete vs immutable log — needs tombstone/compaction strategy.
Debugging tips
- Replay events in test to reproduce production aggregate state exactly.
- Log event type + version on append conflicts.
Optimization strategies
- Snapshot every N events or daily for hot aggregates.
Common misconceptions
- Event Sourcing ≠ Event-Driven. ES stores events as truth; event-driven is communication style.
- Not all tables event-sourced — only aggregates where history matters.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1AdvancedQuestionEvent Sourcing vs audit log table?+
Answer
Follow-up
2AdvancedQuestionEvent schema change?+
Answer
Follow-up
3AdvancedQuestionDelete user under GDPR?+
Answer
Follow-up
4IntermediateQuestionSnapshot purpose?+
Answer
Follow-up
Summary
You can explain event append, replay, snapshots, and audit advantages. Tell the bank 2023 audit story — if you can do that, you own Event Sourcing.