Learn how event sourcing stores state as a log of facts, when it beats CRUD tables, and patterns for projections, versioning, and replays.
Event sourcing stores application state as an append-only sequence of domain events instead of overwriting rows in place. Teams adopt it when audit trails, temporal queries, or reliable integration across services matter more than the simplicity of a single “current state” table.
The core idea
In a CRUD model, a bank account row holds a balance you update. In event sourcing, you store facts: AccountOpened, MoneyDeposited, MoneyWithdrawn. The balance is derived by replaying events (or reading a cached projection). The database of record is the event log; projections are disposable as long as you can rebuild them.
That shift changes how you think about validation, concurrency, and debugging—you debug timelines, not only snapshots.
When event sourcing is worth the complexity
Good fits:
- Regulated domains needing immutable history (finance, healthcare workflows with audit)
- Collaborative domains where “who changed what when” is product-critical
- Integrations where downstream systems consume facts rather than polled state
- Temporal questions such as “what did we know at close of business yesterday?”
Poor fits:
- Simple CRUD admin panels with no audit requirements
- Teams without operational maturity for replay tooling and schema migration
- Read-heavy pages that cannot tolerate projection lag without caching investment
If you only need an audit table beside CRUD, consider outbox + change data capture before full event sourcing.
Building blocks in production
Event store
An append-only log per aggregate (order, account, device). Stores event type, payload version, metadata (user, correlation id), and monotonic sequence numbers. PostgreSQL with careful indexing, Kafka topics keyed by aggregate id, or specialized event stores each work; choose based on ops skills.
Aggregates and commands
Commands express intent (PlaceOrder). The aggregate validates business rules against its history and emits events (OrderPlaced). Keep aggregates small to reduce contention—split hot fields into separate streams if needed.
Projections (read models)
Consumers build query-friendly views: SQL tables, search indexes, caches. Projections may lag briefly; UX should tolerate eventual consistency or use read-your-writes patterns where required.
Snapshots
Replaying thousands of events per request is slow. Periodic snapshots store aggregate state at a sequence number so replay starts mid-stream.
Versioning events without breaking replays
Schemas evolve. Practices that save teams:
- Additive fields with defaults in new event versions
- Upcasters that transform old events to new shapes during replay
- Explicit
eventVersionin payloads - Contract tests that replay golden fixtures in CI
Never edit events in place. Compensating events (PaymentRefunded) correct mistakes; destructive edits destroy trust.
Concurrency and idempotency
Use expected-version checks on append: “save only if current sequence is 41.” Conflicts mean another writer won; retry with refreshed history. Command handlers must be idempotent where transports retry—store processed command ids.
CQRS: optional but common
Command Query Responsibility Segregation separates write models (aggregates) from read models (projections). You can event-source writes without CQRS, but most teams pair them so APIs stay fast while the log stays honest.
Failure modes to rehearse
| Risk | Mitigation |
|---|---|
| Projection bug corrupts reads | Rebuild projection from scratch |
| Poison event blocks consumer | Quarantine + manual fix forward |
| Hot aggregate contention | Split model or shard streams |
| Unbounded log growth | Archival tiers + snapshot policy |
Run game days that rebuild a staging projection from production exports (sanitized).
Minimal example (conceptual)
Command: Deposit($50)
Events: MoneyDeposited { amount: 50, currency: "USD" }
Projection SQL: UPDATE balances SET amount = amount + 50 WHERE id = ...
The projection update is not the source of truth; the event is. If the projection drifts, replay fixes it.
Testing strategy
- Given a list of events, assert aggregate state and emitted events for each command
- Property-based tests on replay: random event order within aggregate should not matter
- Golden replays for each schema version bump
FAQ
Is this the same as event-driven architecture?
Related but not identical. Event-driven systems pass messages between services. Event sourcing is a persistence pattern for state inside a service (often emitting integration events too).
Do I need Kafka?
No. Many teams start with PostgreSQL tables storing events and background workers building projections.
How do deletes work?
Model them as events (UserDeactivated) or tombstone events; physical erasure may still be required for privacy regulations—plan GDPR deletes with legal, not only engineering.
What about reports?
Use projections tailored to reporting, or stream events to a warehouse with batch transforms.
Practical adoption path
Start with one bounded context (billing adjustments, inventory reservations). Keep CRUD elsewhere. Instrument lag, replay time, and consumer errors. Expand only when the team enjoys debugging timelines more than they hate migration scripts.
Tooling that pays off early
Invest in a replay CLI that rebuilds projections for a date range, a web UI or SQL view to inspect an aggregate’s timeline, and metrics on append latency versus consumer lag. Without visibility, event-sourced systems feel “mysterious” to new hires. Document how to fix a bad event: compensating command, manual patch procedure, and who approves it.
Comparing event sourcing to audit tables
Audit tables capture who changed rows; event sourcing makes changes the primary artifact. Audit logs are easier to bolt onto CRUD but weak at rebuilding past state or branching logic. If your compliance ask is only “show me edits,” audit may suffice. If product needs time travel or rich integrations, events earn their keep.
Security and privacy considerations
Event logs can contain PII long after users request deletion. Design retention policies, encryption at rest, and field-level redaction in projections. Role-based access to replay tools is as important as production database permissions—an exported event stream is a backup of your most sensitive history.
Performance tuning notes
Partition streams by aggregate id to keep hot keys from blocking unrelated traffic. Compress large payloads in object storage references rather than inline blobs. For read APIs, serve projections with read replicas; never replay an entire aggregate on every HTTP GET unless aggregates are tiny. Cache snapshot metadata in Redis when cold starts hurt serverless workers.
Sample event envelope
Keep envelopes boring and consistent:
{
"eventId": "uuid",
"aggregateId": "order-9812",
"sequence": 42,
"type": "OrderShipped",
"version": 1,
"occurredAt": "2026-10-20T15:04:00Z",
"payload": { "carrier": "ups", "tracking": "1Z..." }
}
Metadata (correlationId, causationId, actor) belongs beside payload, not buried inside business fields. Consumers rely on stable typing for routing and schema registry entries.
When to stop event sourcing
If teams spend more time fighting replay tooling than shipping features, narrow scope or revert the hottest aggregate to CRUD with an outbox for integrations. Event sourcing is a scalpel, not a default database style.
Read models for operators
Support staff rarely want raw JSON events. Build operator-facing projections with friendly labels, hyperlinks to related aggregates, and safe actions (retry shipment, issue credit) implemented as commands—not ad hoc SQL updates. When operators trust the timeline UI, engineering gets faster feedback on bad events because mistakes surface in minutes instead of quarterly audits.
Treat projections as products: version their schemas, announce breaking changes to internal consumers, and keep backward-compatible readers during migrations just like you would for a public API.
Comments
Loading comments…