Event Sourcing: The Gap Between Theory and Tuesday Afternoon
I built an event-sourced system. Six months later, here's what the blog posts don't tell you.
Event sourcing sounded perfect.
Immutable history. Time travel debugging. Audit logs for free. Rebuild any projection from events.
Six months later, I understand why most teams abandon it.
The Pitch
Traditional systems store current state. Event sourcing stores every state change.
;; Traditional: Store current balance
{:account-id 123 :balance 500}
;; Event sourced: Store what happened
[{:type :account-opened :account-id 123 :initial-balance 1000}
{:type :withdrawal :account-id 123 :amount 300}
{:type :deposit :account-id 123 :amount 200}
{:type :withdrawal :account-id 123 :amount 400}]
The current balance? Replay the events: 1000 - 300 + 200 - 400 = 500.
Want to know the balance on December 15th? Replay events up to that date. Want an audit trail? It's already there.
This is genuinely powerful.
What They Don't Tell You
1. Event Schema Evolution Is Hard
Month 1: Simple withdrawal event.
{:type :withdrawal :account-id 123 :amount 300}
Month 3: Need to track withdrawal reason for compliance.
{:type :withdrawal :account-id 123 :amount 300 :reason "ATM"}
Month 5: Reasons become an enum, not free text.
You now have three versions of the same event type in your store. Every projection, every replay, every new feature needs to handle all three.
Solutions exist (upcasting, event versioning) but they're not simple. And they compound—10 event types × 3 versions each = 30 schemas to maintain.
2. Projections Are The Real Work
Events are append-only. Great for writes. Terrible for reads.
"Show me all accounts with balance > $10,000" requires scanning every event for every account. That's O(events), not O(accounts).
The answer: projections. Materialized views rebuilt from events.
(defn project-account-balance [events]
(reduce
(fn [balance event]
(case (:type event)
:account-opened (:initial-balance event)
:deposit (+ balance (:amount event))
:withdrawal (- balance (:amount event))
balance))
0
events))
Now maintain that for every query pattern. Account balances. Transaction history. Monthly summaries. Fraud detection signals.
Each projection is code. Code has bugs. When projections diverge from events, which is truth?
3. Rebuilding Takes Forever
"Just rebuild the projection from events!"
With 100,000 events, that takes 3 seconds. With 10 million events, that takes 5 minutes. With 500 million events, that takes 4 hours.
We're at 50 million events. Full rebuild: 45 minutes.
Deploying a projection bug fix means 45 minutes of inconsistent data. Or maintaining two projection versions in parallel. Or accepting that some queries are wrong during rebuild.
4. Debugging Is Different (Not Better)
Time travel debugging sounds magical. Reality:
Bug: Account 789 shows wrong balance
Step 1: Pull all 12,847 events for account 789
Step 2: Replay them locally
Step 3: Balance is correct
Step 4: Compare production projection state
Step 5: Projection shows different number
Step 6: Find the projection bug
Step 7: Realize it was fixed in a previous deploy
Step 8: Realize the projection wasn't rebuilt after the fix
Step 9: Realize selective rebuild is 12 minutes for this account
Step 10: Coffee
The audit trail helped. But "replay and compare" is slower than "query the database and see the state."
When It's Worth It
Event sourcing shines when:
- Audit requirements are strict - Financial systems, healthcare, compliance-heavy domains
- Business logic is in state transitions - Workflows, approvals, multi-step processes
- You need to answer "what if" - Retroactive policy changes, backtesting
- Domain experts think in events - "When a customer places an order..." not "The order table has..."
When does event sourcing shine over traditional state storage?
Strict audit requirements, event-driven business logic, retroactive analysis
Financial systems, approval workflows, and "what if" scenarios benefit most
Click to reveal answer
When It's Not Worth It
- CRUD applications - If state transitions are just "update field X," events add ceremony without insight
- High-volume, simple writes - Logging pageviews as events is architectural overkill
- Small teams - The operational overhead requires dedicated attention
- Unknown query patterns - If you don't know how data will be queried, projections become guesswork
What is the biggest operational cost of event sourcing?
Projection maintenance and rebuild time
Each query pattern needs its own projection code, and rebuilding from millions of events can take hours
Click to reveal answer
What I'd Do Differently
Start with event logging, not event sourcing. Log events to an append-only table alongside traditional state. Get the audit trail without the architectural commitment. Migrate to full event sourcing only if you need projection flexibility.
Invest in projection infrastructure early. Rebuilding projections should be push-button, fast, and observable. We underinvested here and paid for it monthly.
Version events from day one. Even if v1 is the only version, the versioning machinery should exist before you need it.
The Verdict
Event sourcing is a legitimate architecture for specific problems. It's not a general-purpose upgrade from traditional systems.
The blog posts make it sound like unlocking a superpower. The reality is trading one set of problems for another.
| Aspect | Traditional | Event Sourced |
|---|---|---|
| Write complexity | Simple | Simple |
| Read complexity | Simple | Complex (projections) |
| Schema changes | Migrate data | Version events |
| Audit trail | Build it | Free |
| Debugging | Query state | Replay events |
| Operational load | Standard | Higher |
Six months in, I'd choose it again for this project. The audit requirements demanded it.
But next project? I'll think harder before reaching for the event store.
Event sourcing isn't hard to implement.
It's hard to operate.
Know the difference before you commit.