Most systems store the current state of things. A user’s balance is $500. An order status is “shipped.” But what if instead of storing the final answer, we stored every event that got us there? That’s event sourcing. And when we combine it with separate models for reading and writing, that’s CQRS.
Traditional CRUD vs Event Sourcing
In a traditional system, when a user deposits $100 into their account, we just update the balance:
UPDATE accounts SET balance = balance + 100 WHERE user_id = 123;
The old balance is gone. We overwrote it. If someone asks “what happened at 3pm yesterday?” — we can’t answer that from the database.
With event sourcing, we store the event instead:
{ event: "MoneyDeposited", userId: 123, amount: 100, timestamp: "..." }
The current balance isn’t stored. We calculate it by replaying all events. Deposit $500, withdraw $200, deposit $100 — replay them and we get $400.
Why Event Sourcing Is Powerful
Complete audit trail — Every change is recorded. We can answer “what was the balance at 3pm on Tuesday?” by replaying events up to that timestamp. For banking, healthcare, and compliance, this is huge.
Replay and rebuild — Found a bug in our billing logic? Fix it, replay all events, and recalculate. We can rebuild the entire state from scratch.
Temporal queries — “Show me the state of this order 2 days ago.” With CRUD, that’s impossible unless we built time travel. With event sourcing, we just replay events up to that point.
Event-driven architecture — Events become the backbone. Other services can subscribe to events and react. “OrderPlaced” can trigger inventory update, email notification, and analytics — all independently.
Debugging — When something goes wrong, we have a complete history of exactly what happened and in what order.
The Event Store
Events are stored in an event store — an append-only log. Events are never modified or deleted. This is important. The immutability is what makes the audit trail trustworthy.
Each event typically has:
- Event type — “OrderPlaced”, “ItemShipped”, “PaymentReceived”
- Aggregate ID — Which entity this event belongs to (order_id: 456)
- Payload — The event data (items, amounts, addresses)
- Timestamp — When it happened
- Version — Sequence number for ordering
Tools like EventStoreDB, Apache Kafka (as an event log), or even a regular database table with an append-only constraint can serve as an event store.
Projections (Materialised Views)
Replaying thousands of events for every read request would be way too slow. So we build projections — pre-computed read models that are kept up to date by processing events.
Think of it like this: the event store is the source of truth. Projections are pre-built answers to common questions.
Events: OrderPlaced → ItemAdded → ItemAdded → PaymentReceived → OrderShipped
Projection 1 (Order Summary): { orderId: 456, items: 2, status: "shipped", total: $89 }
Projection 2 (Revenue Report): { date: "2025-03-15", revenue: $12,450 }
When a new event arrives, the relevant projections are updated. Reads are fast because they hit the projection directly — no replay needed.
What Is CQRS?
CQRS stands for Command Query Responsibility Segregation. In simple language, it means using a different model for reading data than we use for writing data.
In a typical app, the same database model handles both reads and writes. CQRS splits them apart:
- Command side (writes) — Receives commands like “PlaceOrder” or “UpdateProfile.” Validates business rules and produces events or state changes.
- Query side (reads) — Serves read requests using optimised read models (projections). Can use a completely different database or schema.
Why separate them? Because reads and writes have very different needs:
- Writes need strong consistency, validation, and business rules
- Reads need speed, and the shape of data we read is often different from how we store it
How Event Sourcing + CQRS Work Together
They’re a natural pair:
- A command comes in (“PlaceOrder”)
- The command handler validates it and produces events (“OrderPlaced”, “InventoryReserved”)
- Events are saved to the event store (write side)
- Event handlers update projections (read side)
- Read requests query the projections
The write side doesn’t care about read performance. The read side doesn’t care about business rules. Each is optimised for its job.
Real-World Examples
Banking — Every deposit, withdrawal, and transfer is an event. The balance is a projection. Regulators love the complete audit trail.
Shopping cart — ItemAdded, ItemRemoved, QuantityChanged, CouponApplied. We can replay to see exactly how the cart evolved. If a bug miscalculated a discount, we fix the logic and replay.
Version control (Git) — Git is basically event sourcing. Commits are events. The current state of the code is a projection. We can go back to any point in time.
When to Use It
Good fit:
- Systems that need a complete audit trail (finance, healthcare, legal)
- Complex business domains with many state transitions
- Systems where replaying history has real value
- High-read, low-write scenarios where CQRS shines
Bad fit:
- Simple CRUD apps — event sourcing adds enormous complexity for little benefit
- Small projects or MVPs — we’re adding infrastructure overhead we don’t need yet
- Systems where eventual consistency between write and read models is unacceptable
- Teams unfamiliar with the pattern — the learning curve is steep
The Tradeoffs
Event sourcing and CQRS are powerful but come with real costs:
- Complexity — Significantly more moving parts than simple CRUD
- Eventual consistency — Read models lag behind writes. We have to be OK with that
- Event schema evolution — Events are immutable, but our schema will change over time. We need a versioning strategy
- Storage growth — We never delete events, so storage grows forever. Snapshots help (save the current state periodically so we don’t replay from the beginning)
In simple language, event sourcing says “don’t store the answer, store the math.” Instead of saving “balance is $400,” we save every deposit and withdrawal. CQRS says “use different models for reading and writing because they have different needs.” Together, they give us an auditable, replayable, flexible system — but only if the complexity is worth it.