If you’ve spent enough time in backend engineering, you eventually meet the same architectural ghost in different costumes.
Sometimes it arrives wearing a compliance badge. Sometimes it shows up in a fintech system that needs to explain every penny. Sometimes it sneaks into logistics, workflow engines, or “we need to know exactly who changed what and when” enterprise platforms. And very often, that ghost is event sourcing.
At first glance, event sourcing sounds like one of those architecture ideas that was invented by people who enjoy drawing boxes on whiteboards and using the phrase “temporal consistency” with a straight face. But once you dig in, the appeal becomes clear: instead of storing only the latest state, you store every meaningful change as an immutable event.
That means your system doesn’t just know what is true now. It knows how it got there.
And that changes everything.
What Event Sourcing Actually Is
In a traditional CRUD-style backend, the database usually stores the current version of an entity.
For example:
an order is
paidan account balance is
420.00a shipment is
in_transit
That’s simple and efficient. But the past is mostly gone unless you build a separate audit trail, history table, or logging layer.
Event sourcing takes a different route. Instead of storing the current state as the source of truth, you store a sequence of events that represent state changes:
OrderCreatedItemAddedToOrderPaymentAuthorizedOrderShipped
The current state is then derived by replaying those events.
So rather than asking, “What is the current order status?”, event sourcing lets you ask:
What happened?
In what order?
When did it happen?
What business action caused it?
What would the state look like if we replayed the same history today?
That is the core power of the pattern. It is not just persistence. It is durable, replayable history.
Why Auditability Is the Real Superpower
The biggest reason teams adopt event sourcing is usually not because they want to be trendy. It’s because they need a trustworthy record of business history.
In many systems, especially regulated ones, “the current state” is not enough.
Suppose a user says:
“Why was my account frozen?”
“Who approved this transaction?”
“Why did this shipment get rerouted?”
“How did this subscription end up in a canceled state?”
If you only store the latest row in a table, reconstructing the answer may be painful, approximate, or impossible.
With event sourcing, you can often reconstruct the full chain of decisions and state transitions. That makes it much better for:
audit trails
compliance
investigations
dispute resolution
debugging unexpected behavior
temporal analysis
And that last one is especially useful. Event sourcing gives you a kind of time machine. Not a glamorous sci-fi one. More like a very practical office time machine that helps you explain why production behaved like a raccoon in a server room.
The important thing is that the system records facts, not just final outcomes.
A fact like PaymentAuthorized(amount=100) is fundamentally different from a mutable field that simply says status = paid. The event captures the business moment. The field captures only the aftermath.
Replay: The Feature That Makes Event Sourcing Worth the Trouble
Replay is the reason event sourcing is so powerful.
Because the system stores all events, you can rebuild state by replaying them from the beginning. This unlocks several useful capabilities:
Rebuild read models
If your projection logic changes, you can replay events into a fresh read model.Recover from bugs
If a projection was broken for a while, you can fix the handler and replay the stream.Perform temporal debugging
You can inspect how a state evolved at a specific point in time.Create new views without changing the write model
One event stream can support many different read models.
This is a big architectural win. Your write side becomes simple and append-only. Instead of updating many relational records in place, you append events to a log.
That sounds delightfully clean. And it is.
But of course, the backend gods always demand a sacrifice.
The Complexity Moves Somewhere Else
Event sourcing reduces complexity in one area and relocates it into several others.
In a traditional system:
writes are often straightforward
reads are direct
state is explicit
debugging is relatively familiar
In an event-sourced system:
writes are append-only and often simple
reads require projections
consumers must handle ordering and duplicates
rebuilds must be planned
event schemas must evolve safely
debugging can involve replaying history across many versions
So yes, the command side becomes elegant. But now your system depends on the correctness of:
event ordering
idempotent handlers
projection pipelines
serialization formats
schema versioning
consumer behavior
recovery and replay procedures
This is why event sourcing is not “just a better database design.” It is an architectural commitment.
CQRS Often Enters the Room Right Behind It
Event sourcing is often paired with CQRS, or Command Query Responsibility Segregation.
The idea is simple:
Commands change state
Queries read state
In an event-sourced architecture, the command side appends events, while the query side reads from projections built from those events.
Why does this matter?
Because the write model and read model have different needs.
The write model wants strong business rules and correctness.
The read model wants fast, flexible access for the UI, reporting, search, or analytics.
Trying to force one model to serve both often creates awkward compromises. CQRS lets you separate those concerns cleanly.
That said, CQRS is not free either. It adds moving parts. You need to manage consistency between the event stream and the projections, and you need to accept eventual consistency in many cases.
That is usually fine in event-sourced systems, but it must be intentional.
How Replay and Projections Work in Practice
Let’s walk through the mechanics in plain language.
Imagine a shopping cart system.
Events might look like this:
CartCreatedItemAddedItemQuantityChangedItemRemovedCartCheckedOut
You store each event in order.
A projection is a piece of code that consumes those events and builds a read-friendly representation, like:
current cart contents
total price
checkout status
user-facing order history
So the event log is your source of truth, and the projection is a materialized view derived from it.
This is where the design becomes both powerful and tricky.
If the projection code changes, you may need to replay the entire event stream. If one handler misses an event or processes it twice, your read model may become inconsistent. If events arrive out of order, your assumptions may break.
That means event sourcing systems need strong discipline around:
ordering guarantees
deduplication
versioning
monitoring
replay strategy
If CRUD systems are a tidy apartment, event-sourced systems are a research lab. Brilliant things happen there, but you do not casually leave wires dangling.
A Simple Python Example
Here is a small example to show the idea.
from dataclasses import dataclass
from typing import List, Union
@dataclass(frozen=True)
class AccountOpened:
account_id: str
initial_balance: int
@dataclass(frozen=True)
class MoneyDeposited:
account_id: str
amount: int
@dataclass(frozen=True)
class MoneyWithdrawn:
account_id: str
amount: int
Event = Union[AccountOpened, MoneyDeposited, MoneyWithdrawn]
def apply_event(balance: int, event: Event) -> int:
if isinstance(event, AccountOpened):
return event.initial_balance
if isinstance(event, MoneyDeposited):
return balance + event.amount
if isinstance(event, MoneyWithdrawn):
return balance - event.amount
raise ValueError(f"Unknown event: {event}")
def replay(events: List[Event]) -> int:
balance = 0
for event in events:
balance = apply_event(balance, event)
return balance
events = [
AccountOpened(account_id="acc-1", initial_balance=100),
MoneyDeposited(account_id="acc-1", amount=50),
MoneyWithdrawn(account_id="acc-1", amount=20),
]
current_balance = replay(events)
print(current_balance) # 130This example is intentionally tiny, but it shows the essence:
the system stores events
the current state is derived by replaying them
the event list is the durable history
In real systems, events would be persisted in an append-only store, not just held in memory. You would also have projections, handlers, snapshotting, and likely a great deal more ceremony.
Because backend systems never let a good idea remain simple for long.
Snapshotting: The Escape Hatch for Long Histories
If you replay every event from day one every time you need state, the system will eventually become slow and expensive.
That’s why snapshotting exists.
A snapshot is a saved state at a point in time. Instead of replaying ten million events, you start from a recent snapshot and replay only the newer events.
For example:
snapshot at event 9,800,000
replay events 9,800,001 onward
This reduces recovery time and speeds up rebuilding state.
But snapshots are not the source of truth. They are a performance optimization.
That distinction matters. If you treat snapshots as primary data, you lose one of the main benefits of event sourcing: the ability to reconstruct and audit history.
The Hard Problems: Schema Evolution and Versioning
Here is where reality walks into the room with a clipboard.
Events are immutable, which is wonderful. But your business is not immutable. Your product evolves. Fields change. Rules change. Names change. Entire workflows get redesigned because somebody in leadership had a “vision.”
That creates versioning problems.
For example, maybe you originally stored:
{
"event_type": "OrderPlaced",
"order_id": "o-123",
"total": 100
}Later, you decide that total should include currency and tax breakdown:
{
"event_type": "OrderPlaced",
"order_id": "o-123",
"subtotal": 90,
"tax": 10,
"currency": "USD"
}Now what happens when replaying old events?
You need a strategy:
evolve consumers to support multiple versions
transform old events into new shapes
keep backward compatibility
maintain version metadata
test replay against historical data
This is one of the biggest reasons event sourcing is harder than plain relational storage.
With CRUD, you often mutate schema and migrate data. With event sourcing, the old events stay forever, so your new code must understand the old world.
That is powerful. It is also a long-term obligation.
Idempotency: Because Distributed Systems Love Duplicate Messages
If your system processes events through queues, streams, or async consumers, duplicates can happen.
Maybe the consumer crashes after processing but before acknowledging. Maybe the broker retries. Maybe your deployment got moody. Distributed systems contain multitudes, and many of them are duplicates.
That’s why event handlers must often be idempotent.
An idempotent handler can process the same event more than once without causing incorrect state.
For example, instead of blindly incrementing a counter, you might store processed event IDs and skip duplicates.
processed_events = set()
balance = 0
def handle_deposit(event_id: str, amount: int):
global balance
if event_id in processed_events:
return
processed_events.add(event_id)
balance += amountIn real production systems, this logic needs to be durable, transactional, and carefully designed, but the principle remains the same: assume duplicate delivery will happen, and design for it.
Why Event Sourcing Is Great for Some Domains and a Bad Fit for Others
Event sourcing shines when history is the product, or at least a major part of it.
Great fits include:
financial systems
banking and payments
compliance-heavy workflows
insurance claims
logistics and supply chain tracking
procurement systems
complex domain workflows
audit-sensitive admin platforms
In these domains, being able to reconstruct the past is not a luxury. It is often the point.
But for simpler applications? Not so much.
If you are building:
a basic CMS
a small internal dashboard
a CRUD-heavy inventory tool with minimal audit needs
a lightweight SaaS with straightforward state
then event sourcing may be overkill.
You do not need a full replayable history just to manage blog posts unless your blog posts are somehow regulated by a ministry with feelings.
The rule of thumb is straightforward: use event sourcing when history matters enough to justify the extra design and operational complexity.
Tooling Helps, But It Won’t Save You from Bad Architecture
There are excellent tools and frameworks that support event sourcing patterns.
Some of the most well-known include:
EventStoreDB
Apache Kafka
Axon Framework
NATS JetStream
Apache Pulsar
Python libraries like eventsourcing, py-eventsourcing, and similar domain libraries
projection and stream processing tools built around Kafka consumers or stream processors
These tools can help with:
durable event storage
ordered streams
consumer groups
replay
projection pipelines
event transport
operational scaling
But tooling is not the same as architecture.
A great framework can standardize the mechanics, but your team still has to own:
event naming conventions
schema management
consumer idempotency
replay procedures
snapshot policy
observability
testing strategy
domain event design
If those parts are weak, the platform just helps you fail in a more organized way.
Testing Event-Sourced Systems Requires a Different Mindset
Testing event-sourced systems is not just about unit tests on business logic.
You also need to test:
event handlers
projection rebuilds
historical replays
schema migrations
duplicate delivery handling
ordering assumptions
recovery from partial failures
A very useful approach is to test the event stream as the primary artifact.
For example:
given this command sequence
expect this sequence of events
when replayed, expect this projection state
That gives you confidence in both the write side and the derived read side.
This is especially important because many bugs in event sourcing don’t appear at write time. They appear later, when the event stream is replayed, a consumer is rebuilt, or a new projection is introduced.
In other words, the system may look fine until reality asks it to explain itself.
Operational Observability Becomes Non-Negotiable
Event sourcing is not friendly to “we’ll check logs if something breaks.”
You need excellent observability:
event stream metrics
consumer lag
projection freshness
failed event counts
replay duration
schema mismatch alerts
dead-letter queues
version compatibility visibility
Why? Because the health of the system depends not just on whether events are being written, but whether they are being consumed and projected correctly.
A healthy event log with a broken projection is still a broken product.
This is one reason event sourcing often works best in teams that already have mature operational practices. The architecture rewards discipline.
A Practical Mental Model for Choosing Event Sourcing
Ask yourself these questions:
Do we need a durable, inspectable history of business actions?
Do we need to replay history to recover or rebuild state?
Do we need multiple different read models from the same source of truth?
Is traceability a core feature, not just a nice-to-have?
Are we prepared to manage versioning, projections, and replay operations?
Do we have the team maturity to support the complexity?
If the answer to most of these is yes, event sourcing may be a good fit.
If not, a well-designed relational model with audit tables, domain events, or simpler history tracking may be a better trade-off.
And that’s the heart of it: event sourcing is not universally superior. It is selectively superior.
Closing Thoughts
Event sourcing is one of those backend patterns that looks deceptively clean from a distance and deeply opinionated up close.
Its promise is compelling: a durable, replayable history of everything that matters, with strong auditability, traceability, and temporal reasoning. That is not a minor benefit. In the right domain, it is a game changer.
But the price is real.
You trade simple updates for projections. You trade easy debugging for replay discipline. You trade a single model for a whole system of event streams, read models, versioning policies, and operational guardrails.
So the right question is not, “Is event sourcing cool?”
It is, “Does this system need history badly enough to justify the machinery?”
In finance, compliance, logistics, and other history-heavy domains, the answer is often yes. In smaller or simpler systems, it may be a very expensive way to rediscover ordinary state management.
Build it when the past matters.
Avoid it when the past is just noise.
And if you do adopt it, treat the event log like a first-class product asset, not a side effect of persistence. That mindset is what makes the architecture work.
Warmly yours,
The Backend Developers
Come back tomorrow for more backend truths, practical patterns, and the occasional lovingly sarcastic take on systems that refuse to stay simple.









