Event Sourcing: The Gap Between Theory and Tuesday Afternoon

Architect's Journal·January 5, 2025·9 min read

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:

  1. Audit requirements are strict - Financial systems, healthcare, compliance-heavy domains
  2. Business logic is in state transitions - Workflows, approvals, multi-step processes
  3. You need to answer "what if" - Retroactive policy changes, backtesting
  4. 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

  1. CRUD applications - If state transitions are just "update field X," events add ceremony without insight
  2. High-volume, simple writes - Logging pageviews as events is architectural overkill
  3. Small teams - The operational overhead requires dedicated attention
  4. 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.

AspectTraditionalEvent Sourced
Write complexitySimpleSimple
Read complexitySimpleComplex (projections)
Schema changesMigrate dataVersion events
Audit trailBuild itFree
DebuggingQuery stateReplay events
Operational loadStandardHigher

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.