A traditional DB stores the current state: user 42 has balance $150. How they got there is gone. Event sourcing inverts this: you store the stream of changes (deposits, withdrawals, transfers), and "current balance" is a derived projection. It's how banks actually work — a ledger of transactions, not a balance field.
CQRS (Command Query Responsibility Segregation) splits the system into a write side (commands that produce events) and one or more read sides (projections optimized for each query shape). Pair the two and you get auditability, temporal queries, independent scaling of reads and writes.
Useful in: financial systems, order management, collaborative editing, anywhere "how did we get here?" matters.
02
Traditional vs event-sourced
Traditional (state-stored)
Current state = source of truth
UPDATE users SET balance = 150 WHERE id = 42. The old value is gone. Audit logs? Separate table, may drift, often incomplete. Bug corrupted the state? You can't easily replay to find when.
Event-sourced
Events are source of truth; state is derived
Append: {id: "e7", user: 42, type: "deposit", amt: 50}. Current balance = sum of all deposit/withdraw events. Full history is the only storage. Audit is free. Bugs replayable.
03
The three parts
Commands. User intents — "transfer $50 from A to B." Validated against current state. If valid, emit events.
Events. Facts of what happened — "$50 withdrawn from A (event id e7, timestamp t)" and "$50 deposited to B (e8, t)". Immutable. Appended to the event log (Kafka, EventStore, Postgres table).
Projections. Read-side views computed by replaying events. Current-balances view. Transactions-by-day view. Suspicious-activity view. Each tuned for its query pattern.
Saga with compensating actions
# Order saga: reserve → charge → ship → notify
# On failure, compensate completed steps in reverse order.
class Saga:
def __init__(self, steps):
self.steps = steps # [(action, compensation), ...]
self.completed = []
def run(self, ctx):
for action, compensate in self.steps:
try:
action(ctx)
self.completed.append(compensate)
except Exception as e:
# Roll back completed steps in reverse
for c in reversed(self.completed):
try: c(ctx)
except: pass # compensations themselves must be idempotent
raise
# Use Saga when: transactions span services, low contention, some slack OK
# Don't use Saga when: strict ACID required, low value (just pay the cost of 2PC)
Event Sourcing + CQRSMermaid
flowchart LR
C[Client] -->|Command: transfer| W[Write Model]
W -->|validate, emit| E[(Event Log)]
E -->|subscribe| P1[Balances Projection]
E -->|subscribe| P2[Audit Projection]
E -->|subscribe| P3[Analytics Projection]
C -->|Query balance| P1
C -->|Query audit| P2
04
What you gain, what you pay
Gains:
Free audit trail — every change traceable to a specific event with actor and timestamp.
Time travel — replay events up to any past instant to reproduce historical state (great for debugging, regulatory reports).
New projections cost nothing — add a view later, replay events to build it. No data migration.
Read and write scale independently — beefy event-log cluster, many lightweight projection services.
Natural fit for pub/sub — events flow downstream to analytics, notifications, search indexing.
Costs:
Every query goes through a projection. Eventual consistency — just wrote an event? The balance view may lag 50ms.
Event schema evolution is a nightmare. You rename a field; old events in the log still have the old name. Versioning + upcasters forever.
Storage grows linearly with activity, not with current state. Compaction strategies needed (snapshots).
Conceptual weight — the team has to think in events, not tables. New hires hate it for 6 months.
05
Deep dive — snapshots and replay
A user has 10,000 events over 5 years. Every time you query their balance, replaying 10k events takes seconds. Solution: snapshots.
Periodically (every 100 events, or daily), compute the current state and persist it with the event ID up to which it's been applied. When querying: load the latest snapshot, apply events after that snapshot's event ID. Balance computation goes from "replay 10,000" to "replay 20." Latency becomes constant.
Snapshots are derived data — you can always rebuild from events, so their storage is optional and they can be regenerated after a bug fix. This keeps the event log as the single source of truth while making queries practical.
Rule of thumb
Don't use event sourcing for a CRUD app. The operational overhead outweighs the benefits. Use it when the history matters as much as the current state — finance, legal, healthcare, audit-heavy domains. For everything else, traditional state storage with an outbox pattern gives you 80% of the benefit.
06
Real-world
LMAX Exchange
Event-sourced trading
Matching engine state is the replay of all orders. Sub-millisecond matching latencies. Reprocess the log to rebuild state on restart.
Stripe ledger
Immutable append-only
Every financial event (charge, refund, transfer) appended to a ledger. Balances derived. Correct by construction.
EventStore / Axon
Purpose-built DBs
Specialized stores for event-sourced apps. Snapshots, projections, subscription APIs built-in.
Kafka Streams / Flink
Streaming projections
Build materialized views from Kafka topics. CQRS made operational.
07
Used in problems
Stock exchange is naturally event-sourced (every order is an event). Payment gateway uses event sourcing for the ledger. E-commerce order state is increasingly event-sourced for refund/return workflows.