Q51 — What is event sourcing — what do you gain and what does it cost?
What do normal databases store?
In a conventional system — what's sometimes called "state-based" persistence — you store the current state of an entity. A user record has a balance column. When the user deposits $100, you run UPDATE accounts SET balance = balance + 100 WHERE id = ?. The new balance overwrites the old one. The $100 deposit is gone — only the resulting balance remains.
This is simple and fast. But notice what's lost: you no longer know how you got to the current balance. Was it one large deposit or many small ones? Were there withdrawals in between? Did a bug ever set the balance incorrectly? The database has no memory of the path — only the destination.
Event sourcing inverts this model: don't store state, store the events that caused the state. The database is not a snapshot of current values; it's an append-only log of everything that ever happened.
What is an event store?
An event store is an append-only log of immutable events. Each event represents something that happened — a fact, expressed in the past tense: "AccountOpened," "MoneyDeposited," "MoneyWithdrawn," "AccountFrozen."
Every event has:
- An aggregate ID (which entity it belongs to — e.g., account ID 42)
- An event type
- A payload (the data relevant to this event — amount, timestamp, reason)
- A version/sequence number (position in the stream for this aggregate)
To reconstruct the current state of an account, you replay its event stream from the beginning: start with an empty account, apply "AccountOpened" (initialize), apply each "MoneyDeposited" and "MoneyWithdrawn" in order, and the final state emerges. The current balance is derived, not stored.
Event stream for account 42:
v1: AccountOpened { initial_balance: 0 }
v2: MoneyDeposited { amount: 1000 }
v3: MoneyWithdrawn { amount: 200 }
v4: MoneyDeposited { amount: 500 }
→ Current balance = 0 + 1000 - 200 + 500 = 1300
The event stream is immutable — you never modify or delete events. You can only append new ones.
What do you gain?
Complete audit log, for free. Because every state change is an event in the log, you have a complete, immutable history of everything that ever happened to every entity. There's no need to build a separate audit trail — the event store is the audit trail. For financial systems, healthcare records, legal applications, and compliance-heavy domains, this is enormously valuable. You can answer: "What was the balance at 2:37 PM on March 15th?" — just replay events up to that timestamp.
Temporal queries. You can reconstruct the state of any entity at any point in time. "What did the shopping cart look like before the customer removed the item?" Just replay to an earlier version. This is impossible in a state-based system without explicit time-travel columns or audit tables.
Debugging and root cause analysis. When you find a bug, you can replay the exact event sequence that led to the incorrect state. You can reproduce production bugs locally by replaying production events against a fixed version of the code.
Event-driven integration, for free. The event log is a natural source of events for other services. You don't need the outbox pattern (Q49) because the event store is the outbox — publishing events to other services is just reading from the event log. Any new service can subscribe to the event stream and build its own view of the data from the full history.
Time travel and retroactive computation. If you add a new projection or reporting requirement, you can replay all historical events through new code to build the view from scratch. With a state-based database, adding a new column that requires historical computation is a hard problem. With event sourcing, replay the events through logic that computes the column's value.
What does it cost?
Querying current state is expensive without projections. If you need to answer "what is account 42's current balance?" you can't just SELECT balance FROM accounts WHERE id = 42. You have to replay the event stream. For long-lived entities with thousands of events, this is slow.
The solution is projections (also called read models or views): a separate, precomputed representation of current state, built by consuming the event stream. A background process reads events and maintains a current_balance table. Reads are fast — you read from the projection. Writes go to the event store. Projections are always derived from events; if they get out of sync, you rebuild them by replaying.
This is a significant architectural overhead. You now have an event store (write model) and one or more projections (read models) — two separate representations of the same data that must be kept in sync. This is inherently CQRS (Q52) — commands write to the event store, queries read from projections.
Schema evolution is hard. With a state-based database, migrating the schema means altering the table and potentially backfilling data. With event sourcing, events are immutable — you can't alter the past. If you change the payload format of an event type, you now have two versions of that event in the log. Your event-handling code must handle both. Over years and many schema changes, this becomes complex to manage. Techniques: upcasting (transform old events to the new format on read), versioned event types, schema registries.
Eventual consistency in projections. Since projections are built asynchronously from the event stream, they are eventually consistent with the event store. A command that writes an event may not be reflected in the projection immediately. For most use cases this is fine — the command succeeded, the UI can optimistically update, and the projection catches up within milliseconds. But for strict read-your-own-writes requirements (Q35), you need extra care: either query the event store directly for immediate consistency, or use a synchronous projection update path.
Conceptual complexity. Event sourcing requires a fundamentally different mental model. Developers used to SELECT / INSERT / UPDATE / DELETE need to learn to think in terms of events, aggregates, commands, and projections. The learning curve is real, and not every team is ready for it.
When is event sourcing the right choice?
Event sourcing shines when:
- Audit history is a first-class requirement (finance, healthcare, legal)
- Time-travel queries are needed
- The domain is naturally event-driven (financial transactions, order lifecycle)
- Multiple downstream consumers need to build different views of the same data
- Replay and retroactive computation are valuable
Event sourcing is the wrong choice when:
- The domain is simple CRUD with no meaningful history
- The team is small and the overhead of projections + schema evolution is not justified
- Querying current state flexibly (ad-hoc analytics, complex joins) is a core requirement — projections give you predetermined views, not flexible queries
- You need strong consistency across reads and writes without extra complexity
The mental model
Normal (state-based):
Store current state → UPDATE overwrites → history is gone
Simple, fast → but no audit trail, no time travel, no replayability
Event sourcing:
Store events (immutable, append-only) → current state is derived by replay
"AccountOpened" + "MoneyDeposited" + "MoneyWithdrawn" → balance = X
Event store:
Append-only log of facts (past tense events)
Each event: aggregate_id, type, payload, sequence_number
Never modified or deleted
What you gain:
✅ Complete audit log — the log IS the history
✅ Time travel — replay to any point in time
✅ Debugging via replay — reproduce exact state from events
✅ Event-driven integration — event store IS the outbox
✅ Retroactive computation — replay events through new code
What it costs:
⚠️ Querying current state = replay → slow without projections
→ Need separate read models (projections) maintained from event stream
⚠️ Schema evolution — events are immutable → must handle multiple versions
⚠️ Eventual consistency in projections — not always read-your-own-writes
⚠️ Conceptual complexity — events, aggregates, commands, projections
Projections (read models):
Background process consumes event stream → maintains current-state table
Reads go to projection (fast), writes go to event store
If projection breaks → rebuild by replaying from event store
Use when:
Audit is mandatory (finance, healthcare, legal)
Time travel / retroactive computation needed
Naturally event-driven domain
Don't use when:
Simple CRUD, no meaningful history
Team is not ready for the complexity overhead
Ad-hoc flexible queries are primary requirement
Event sourcing treats the past as a first-class citizen. Instead of overwriting history with each update, it accumulates history as the primary store. The current state is a view computed from that history. This gives you perfect observability and replayability — at the cost of significant architectural complexity that most systems don't need.