What is event sourcing — storing events instead of state — and what do you gain and lose?
Instead of overwriting a row, event sourcing appends what happened and rebuilds current state by replaying history -- a full audit log becomes the source of truth itself.
LEARN BY DOINGPut your intuition to work3 steps
What is event sourcing — storing events instead of state — and what do you gain and lose?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#From storing state to storing history
A conventional CRUD system stores current state: a row holds a customer's balance, and every operation overwrites it in place. The previous value is gone the moment the UPDATE commits -- if you need to know how a balance reached its current value, you need a separate audit log, and that log is only ever as complete as whoever remembered to write to it. Event sourcing inverts this: instead of storing state, you store the sequence of events that produced it, and current state becomes a derived value, computed by replaying (folding over) that sequence.
An aggregate in an event-sourced system is rebuilt by starting from an empty state and applying every event that has ever happened to it, in order. That sounds expensive, and at scale it can be, but the state itself is never the durable artifact -- the events are. You can always throw the current state away and recompute it, which is a strange but powerful guarantee to have.
#An event-sourced aggregate, end to end
The clearest way to see this is a small bank account: commands are validated against the in-memory state produced by replay, and a valid command produces a new event rather than mutating a field directly.
public sealed interface Event permits AccountOpened, MoneyDeposited, MoneyWithdrawn, AccountClosed {}public record AccountOpened(String accountId) implements Event {}public record MoneyDeposited(String accountId, long amountCents) implements Event {}public record MoneyWithdrawn(String accountId, long amountCents) implements Event {}public record AccountClosed(String accountId) implements Event {}public final class BankAccount {private String accountId;private long balanceCents;private boolean closed;// Rebuild state by replaying history -- nothing is ever read from// a 'current state' column, only from the events that produced it.public static BankAccount replay(List<Event> history) {BankAccount account = new BankAccount();for (Event event : history) {account.apply(event);}return account;}private void apply(Event event) {if (event instanceof AccountOpened e) {this.accountId = e.accountId();this.balanceCents = 0;this.closed = false;} else if (event instanceof MoneyDeposited e) {this.balanceCents += e.amountCents();} else if (event instanceof MoneyWithdrawn e) {this.balanceCents -= e.amountCents();} else if (event instanceof AccountClosed e) {this.closed = true;}}// Command handling: validate against the replayed state, then// produce a new event instead of mutating a column directly.public MoneyWithdrawn withdraw(long amountCents) {if (closed) {throw new IllegalStateException("account is closed");}if (amountCents > balanceCents) {throw new IllegalStateException("insufficient funds");}MoneyWithdrawn event = new MoneyWithdrawn(accountId, amountCents);apply(event);return event;}}
Nothing here reads or writes a balance column. withdraw only produces an event after checking it against state that was itself derived from prior events -- the event store is the only thing that has to survive a restart.
#What event sourcing buys you
- A complete audit trail by construction. You don't add logging around the state changes -- the events are the log, and it is impossible for it to drift out of sync with reality.
- Time travel. Rebuilding state as of any point in the past means replaying events only up to that timestamp -- useful for debugging ('what did this account look like before the disputed charge') and for compliance.
- New read models for free, retroactively. If a new reporting need shows up next year, you can build a brand-new projection and populate it by replaying events you already have, without having tracked that need from day one.
- Natural event-driven integration. Downstream systems that want to react to
MoneyWithdrawnare subscribing to something you already produce, not to an inferred diff of before/after state.
#What it costs you
| Aspect | Traditional CRUD | Event sourcing |
|---|---|---|
| Source of truth | Current row state | Ordered sequence of immutable events |
| History | Usually lost unless logged separately | Built in, by construction |
| Point-in-time state | Hard, needs extra versioning | Natural -- replay up to a timestamp |
| Ad-hoc queries | Direct SQL against current state | Usually needs a projection (often paired with CQRS) |
| Fixing bad data | UPDATE the row | Append a compensating event -- events are immutable |
| Storage growth | Bounded by entity count | Grows with every state change; needs snapshotting eventually |