Event Sourcing: Building Auditable Systems
Table of Contents
- Introduction
- What Event Sourcing Means
- State-Based vs Event-Based Thinking
- A Simple Example
- Projections: Turning Events into Read Models
- Why Use Event Sourcing?
- Trade-Offs
- When Event Sourcing Fits
- Common Mistakes
- Checklist
- FAQ
- Conclusion
Introduction
Most business applications store the current state of every transaction or change that occurs. For example, a user record holds the latest email address. An order holds the latest status. An account holds the current balance.
Event sourcing starts from a different question: what if the most important thing to store is not only the latest state, but the history of changes that created it?
Instead of storing only “order status is shipped”, an event-sourced system stores facts such as OrderCreated, PaymentCaptured, AddressChanged, and OrderShipped. The current state can be rebuilt from those events.
This pattern is a great fit for systems that require an audit trail, business history, and traceable decisions. However, this pattern is sometimes overused, especially for applications that do not demand such complexity. The most important thing is to understand both the pros and cons.
What Event Sourcing Means
Event sourcing stores a sequence of immutable business events as the source of truth.
An event is something that already happened:
AccountOpenedMoneyDepositedMoneyWithdrawnOrderCancelledSubscriptionRenewed
Event naming is crucial. Events are facts, not commands, so use past tense verbs. For example, CancelOrder (Cancel Order) is a request, whereas OrderCancelled (Order Cancelled) is the fact that the system has received and recorded that cancellation.
State-Based vs Event-Based Thinking
In a state-based model, you usually store the latest shape:
| account_id | balance |
|---|---|
| A100 | 175 |
In an event-based model, you store the changes:
| sequence | event_type | amount |
|---|---|---|
| 1 | AccountOpened | 0 |
| 2 | MoneyDeposited | 100 |
| 3 | MoneyDeposited | 75 |
The current balance is derived from the event stream:
0 + 100 + 75 = 175
The event stream explains how the system reached the current state.
A Simple Example
Imagine an e-wallet system. A traditional table might store:
| wallet_id | balance |
|---|---|
| W01 | 120 |
That value is useful, but it does not explain what happened. With event sourcing, the wallet has a stream of events:
| version | event_type | amount | note |
|---|---|---|---|
| 1 | WalletCreated | 0 | initial wallet |
| 2 | FundsAdded | 200 | top-up |
| 3 | FundsSpent | 80 | purchase |
The current state can be rebuilt by replaying the following event stream:
200 - 80 = 120
If a user asks why the balance is 120, the system can answer with history instead of guesswork.
Projections: Turning Events into Read Models
Applications still need fast read operations. We usually do not want to replay every event every time a user opens a dashboard.
That is why event-sourced systems use projections. A projection is a view or read model built from an event stream. Projections read events and build a model that is fast to query. This pattern pairs closely with the CQRS Pattern to separate write and read workflows in a structured way.
| Event | Projection update |
|---|---|
FundsAdded(200) | increase wallet balance by 200 |
FundsSpent(80) | decrease wallet balance by 80 |
FundsAdded(50) | increase wallet balance by 50 |
The projection might look like a normal table:
| wallet_id | current_balance | last_event_version |
|---|---|---|
| W01 | 170 | 4 |
The event store remains the source of truth. The projection is optimized for reading.
Why Use Event Sourcing?
Auditability
Event sourcing automatically provides you with a complete audit trail. You can see what happened, when it happened, and who triggered it.
Business history
Some domains care deeply about or even require transaction history: banking, billing, inventory, compliance, insurance, and approval workflows.
Debugging complex behavior
When state looks unusual, events help trace the sequence of transactions that produced it. This is far clearer than only seeing the final row.
Rebuilding read models
If you need a new report, projection, or analytics model, you can rebuild it from existing events.
Trade-Offs
Event sourcing adds power, but it also adds operational and design complexity.
| Benefit | Cost |
|---|---|
| Strong audit trail | More complex data model |
| Rebuildable read models | Projection lag and consistency concerns |
| Clear business history | Event versioning over time |
| Better debugging | More infrastructure and tooling |
The main question is not “is event sourcing cool?” The better question is “does this domain need history enough for the complexity to be worth it?”
When Event Sourcing Fits
Event sourcing is a great fit for cases where:
- business events are more important than current state alone
- audit trails are required
- decisions need to be explained later
- workflows involve many transitions
- reports may be rebuilt from historical facts
- the team can handle eventual consistency
It is usually a poor fit for simple CRUD screens where the current value is enough and history has little value.
Common Mistakes
Treating events like database logs
Events should describe business facts, not low-level database operations. A good example: EmailChanged. A poor example: UserRowUpdated, because it does not explain the business fact, only that data changed.
Changing old events casually
Events are historical facts. If an event schema needs to evolve, use versioning or transformation carefully instead of rewriting old events casually, which will break your audit trail.
Ignoring projection failure
Projections can fail or fall behind. Track progress, retries, and lag. Ensure that failed projections do not corrupt read models and remain up to date.
Using event sourcing everywhere
Not every table needs an event stream. Use this pattern where history is truly valuable, and do not force it onto systems that do not need it.
Forgetting idempotency
What is Idempotency? Idempotency is the property of an operation where it can be executed multiple times without changing the result beyond the initial application.
For example: x = x + 1. If run repeatedly, the result changes each time. However, x = x (constant assignment) produces the exact same value regardless of how many times it runs.
In the context of event sourcing, projection handlers must be safe to replay. If duplicate processing occurs, it must not corrupt the read model. Idempotency is key to preventing duplicate data and ensuring data consistency. For a deeper look into handling duplicate requests at the API level, read our article on the Idempotency Key.
Checklist
- Define events as business facts.
- Keep events immutable.
- Store event order per aggregate or stream.
- Design projections for read use cases.
- Track projection version and lag.
- Plan event schema evolution.
- Make handlers idempotent.
- Decide snapshot strategy only when replay becomes expensive.
- Use event sourcing for domains where history matters.
- Avoid applying it to simple CRUD by default.
FAQ
Is event sourcing the same as an audit log?
No. An audit log records history beside the normal state model. In event sourcing, events are the source of truth and current state is derived.
Do I need CQRS for event sourcing?
Not always, but they are often used together. CQRS separates write models from read models, which fits naturally with event streams and projections.
Can events be changed?
Old events should be treated as immutable. If meaning changes over time, introduce new event versions or transformation logic.
Is event sourcing only for microservices?
No. Event sourcing can be used in a modular monolith too. The pattern is about persistence and history, not service count. However, in distributed architectures like Microservices Architecture or Hexagonal Architecture, event sourcing is frequently combined with the Saga Pattern to manage transactions across services.
Conclusion
Event sourcing is useful when the story behind the data matters just as much as the latest data. This pattern provides auditability, business history, and the ability to build read models from facts.
However, event sourcing is neither easy nor free. It requires significant knowledge and effort. Use it in domains that genuinely require historical truth, and stick to simpler persistence for parts of the system that only need current state.
How about the system you are currently building? Have you ever implemented event sourcing or custom audit trails in your project?
Feel free to share your experiences, questions, or thoughts in the comments section below!
Related Articles
Keep reading within the same topic.
Saga Pattern: Managing Distributed Transactions
Learn the saga pattern for distributed transactions, including choreography, orchestration, compensating actions, failure handling, and practical trade-offs.
Circuit Breaker Pattern: Building Resilient Systems
Learn the circuit breaker pattern with closed, open, and half-open states, retries, timeouts, fallbacks, observability, and production trade-offs.
Hexagonal Architecture: Ports and Adapters Explained
Understand hexagonal architecture with ports, adapters, dependency direction, testing benefits, examples, trade-offs, and practical implementation guidance.
NoSQL vs Relational: Choosing the Right Database for Your Project
A practical framework for choosing relational or NoSQL databases from data shape, queries, consistency, and operational needs.