Event Sourcing Is Not a Database Pattern
For: A CTO at a 40-person B2B SaaS company whose audit trail, undo functionality, and compliance reporting are all held together by fragile application-layer logic — and who keeps hearing 'event sourcing' as the fix but cannot find an explanation that isn't either a PhD thesis or a toy example with no production caveats
Event sourcing is not storing your current state with a history log attached. It is storing the history and treating current state as a disposable, rebuildable projection of that history. If your CRUD tables are still the source of truth and the event log is a sidecar audit stream, you have not done event sourcing — you have added a second system to keep in sync, and you now own the failure modes of both.
That inversion — history is the truth, state is the cache — is the whole idea. Everything else is plumbing.
The problem it actually solves
Take a typical B2B SaaS backend. You have an invoices table. A user edits an invoice. You UPDATE the row. The old values are gone. Now compliance asks who changed the tax rate on invoice #4471 last March, and why. So you bolt on an audit_log table. Then finance wants undo, so you add a versions table. Then a customer disputes a charge and you need to know what the invoice looked like when they saw it, so you add snapshots. Then someone wants to replay how a bug corrupted 300 rows.
Each of these features is a separate, fragile mechanism trying to reconstruct information you threw away the moment you ran UPDATE. That is the problem event sourcing solves. Not performance. Not scalability. Information loss at the point of write.
The analogy: your bank account
Your bank does not store your balance. It stores every deposit and withdrawal, and computes your balance by summing them. If someone disputes a transaction, the bank does not edit your balance — it appends a reversal. Your balance today is a function of every event that ever happened to your account.
If the bank stored only your current balance and overwrote it on every transaction, the entire concept of banking would collapse. You could not audit, dispute, reconcile, or rebuild. That is what your CRUD application does to every entity it manages.
Event sourcing says: model your domain the way a bank models an account. The events are the truth. The balance is a view.
A minimal worked example
Say you have a subscription entity. In a CRUD model:
subscriptions
id | plan | status | seats | updated_at
42 | pro | active | 25 | 2024-11-03
In an event-sourced model, that row does not exist as a source of truth. Instead you have an append-only stream:
events (stream: subscription-42)
1 SubscriptionCreated { plan: starter, seats: 5 }
2 PlanUpgraded { from: starter, to: pro }
3 SeatsAdded { count: 20 }
4 PaymentFailed { attempt: 1 }
5 PaymentRecovered { }
To get the current state, you fold the events through a pure function:
state = events.reduce(apply, initialState)
The subscriptions table can still exist — but only as a projection. A read model. A cache built by replaying events into a shape convenient for querying. If you drop the table and rebuild it from the event stream, you get the same answer. If you cannot, you are not event sourced.
Now watch what falls out for free:
- Audit trail: the events are the audit trail. No parallel table.
- Undo: append a compensating event. History is preserved.
- Time travel: fold events up to a timestamp to see what state looked like then.
- New read models: product wants a new dashboard? Write a new projection, replay the log, done.
- Bug recovery: a bad deploy corrupted your projections? Drop them and replay.
None of these are features you build. They are consequences of the model.
The gotchas nobody puts on the landing page
Event sourcing is not free. Here is what the introductory articles skip.
1. Events are forever, and so are your mistakes modeling them
Once PlanUpgraded is in production and you have a million of them, you cannot rename the field. You cannot change its semantics. You will need event versioning (PlanUpgradedV2) and upcasters that translate old events into the new shape at read time. Schema evolution is a discipline, not an afterthought.
2. Projections are eventually consistent
When you write an event, the read model updates asynchronously. A user creates an invoice and immediately queries the list — the invoice might not be there yet. You either accept this in the UX, read your own writes from the write side, or build synchronous projections (which reintroduce the coupling event sourcing was meant to break).
3. Querying is harder
You cannot SELECT * FROM subscriptions WHERE plan = 'pro' AND seats > 10 against an event stream. You query the projection. Which means every question you want to ask needs a projection designed for it. Ad-hoc SQL against production becomes ad-hoc SQL against a read model that may be stale, incomplete, or missing entirely.
4. GDPR and the right to be forgotten
Append-only logs and "delete this user's data" are natural enemies. The usual pattern is crypto-shredding: encrypt PII in events with a per-subject key, then delete the key. The event remains; its contents become unreadable. You have to design this in from day one.
5. It is a modeling exercise, not a library import
The hard part is not picking EventStoreDB or Kafka or Postgres-as-a-log. The hard part is figuring out what your events are. "UserUpdated" is a bad event — it is a CRUD update in a costume. "EmailChanged", "PasswordReset", "AccountSuspended" are events. They describe intent. If your events read like database diffs, you have not modeled a domain, you have logged your writes.
6. Debugging a fold is not debugging a row
When a projection is wrong, the bug could be in any event, any apply function, or the order of events. New engineers who grew up on "look at the row" need to learn "replay this stream in a test and step through the fold." This is a real cost.
Event sourcing vs CRUD: when to use which
Event sourcing is worth the tax when:
- The history is a product requirement, not a nice-to-have. Regulated industries: finance, healthcare, insurance. See how we thought about auditability in a lending platform with KYC and credit scoring or in an accounting SaaS where every ledger mutation matters.
- You need to answer questions like "what did this look like on day X" or "why did this state change."
- Multiple read models are natural — a dashboard, a search index, a data warehouse, all fed from the same events.
- Your domain is intrinsically event-shaped: orders, payments, workflows, state machines.
Stick with CRUD when:
- Your domain is mostly reference data — catalogs, configurations, content.
- History is not a business concern and your existing audit table is genuinely fine.
- Your team is small, the domain is not deeply modeled yet, and you need to ship. Premature event sourcing is worse than premature optimization because it is harder to walk back.
- You do not have someone on the team who has done it before. The failure mode of learning event sourcing on a production system is a hybrid mess that satisfies neither model.
The mental inversion, one more time
If you take one thing from this: your database tables are not the truth with an event log bolted on. The event log is the truth, and your tables are a query-optimized view of it that you should be able to delete and rebuild. Until your team believes that — until dropping the subscriptions table on a Friday and rebuilding it from events on a Monday feels routine — you have not adopted event sourcing. You have added a table.
That is why hybrid implementations disappoint. They ask the CRUD tables to be authoritative and the event log to be authoritative simultaneously, and reality only tolerates one source of truth per fact. Pick one. If history matters, pick the log, and make everything downstream a projection of it.
Frequently Asked Questions
Do I need Kafka or EventStoreDB to do event sourcing?
No. Postgres with an append-only events table, a sequence for ordering, and a projection worker will get most teams surprisingly far. Dedicated event stores add value at scale and give you things like optimistic concurrency and subscriptions out of the box, but the pattern is about how you model truth, not which product you buy.
Can I add event sourcing to an existing CRUD system incrementally?
Yes, but scope it. Pick one bounded context where history is genuinely valuable — billing, subscriptions, workflow state — and make that context event-sourced end to end. Keep the rest CRUD. Do not sprinkle events across the whole codebase; that produces the hybrid mess this post warns about.
How does event sourcing relate to CQRS?
CQRS separates the model you write to from the models you read from. Event sourcing is a natural write model for CQRS because events are what you append on the write side, and projections are the read side. You can do CQRS without event sourcing, and technically event sourcing without CQRS, but they pair well and most production systems use both.
What is the biggest mistake teams make adopting event sourcing?
Modeling events as CRUD diffs — "UserUpdated" with a bag of changed fields — instead of as domain intents like "EmailChanged" or "SubscriptionCancelled". Diff-shaped events give you the storage overhead of event sourcing with none of the semantic value. Your events should read like a story of what happened in the business, not a database changelog.
How do we evaluate whether event sourcing is right for our system?
Look at how much of your current codebase exists only to reconstruct information you threw away on write — audit tables, version tables, snapshotting, replay tooling, forensic queries. If that surface area is large and growing, event sourcing likely pays for itself. If you want an outside read on your specific architecture, contact CodeNicely for a personalized assessment.
Found this useful? CodeNicely publishes engineering and product playbooks weekly. Browse the archive or tell us what you're building.
_1751731246795-BygAaJJK.png)