There are two kinds of backend systems in this world:
The ones that tell you what the current state is.
The ones that tell you what happened, when it happened, and occasionally make you question your life choices.
Event sourcing belongs firmly in the second category.
If you’ve ever built a system where someone asked, “How did we end up here?” and your answer was a vague shrug followed by a SQL query that still didn’t explain anything, event sourcing starts sounding very attractive. Instead of overwriting state, you append immutable events to a log. Those events become the source of truth. The current state is just a reconstruction of history.
That sounds elegant, and in many cases it absolutely is. But like all elegant backend ideas, it arrives wearing a tuxedo and carrying a toolbox full of trade-offs.
In this post, we’ll walk through what event sourcing is, why teams adopt it, how replays work, where snapshots and projections fit in, and why the operational burden can feel like adopting a very smart, very stubborn pet.
What Event Sourcing Actually Means
In a traditional CRUD-style backend, you usually store the latest version of an entity. A customer changes their address? Update the row. An order is shipped? Flip the status field. A payment fails? Update a flag and move on with your day.
Event sourcing takes a different approach.
Instead of storing the final state directly, you store the sequence of events that led there:
CustomerCreatedAddressUpdatedOrderPlacedPaymentAuthorizedOrderShipped
Each event is immutable. You do not edit history. You add to it.
The current state is derived by replaying those events in order. If you want to know the state of an order, you rebuild it from the event stream. If you want to know what happened last Tuesday at 3:17 PM, the log can tell you. If you want to reconstruct the system as it looked before a bug was introduced, you can often replay up to a point in time and inspect the result.
This is the core value proposition: auditability and reconstructability.
And that is why event sourcing shows up so often in domains like finance, logistics, healthcare, compliance-heavy applications, dispute resolution, and anywhere the history matters as much as the present.
Why Teams Adopt Event Sourcing
Let’s be honest: nobody wakes up excited to add complexity to their backend for fun. Well, not nobody, but those people should be monitored.
Teams adopt event sourcing for very specific reasons:
Auditability
You can inspect the full history of a domain object or aggregate. This is useful when the business needs a complete record of who did what and when.
Temporal reconstruction
You can rebuild the system as it existed at a point in time. That’s incredibly valuable for debugging, compliance, and forensic analysis.
Replayability
You can reprocess historical events to build new read models, recover from bugs, or support new reporting needs without changing the original write path.
Business meaning in history
Sometimes the sequence itself is part of the domain. In a trading system, a supply chain, or a billing workflow, the order and nature of changes matter deeply.
Better traceability across distributed systems
Events can become the shared language of the system. This makes it easier to reason about long-running processes and eventual consistency.
So yes, event sourcing is powerful. But power in software is never free. The bill arrives later, often in the form of operational complexity and debugging headaches.
The Event Stream as Source of Truth
The mental model is simple: the event log is the truth, and everything else is a projection of that truth.
That means your write model does not say, “the order is shipped.” It says, “an OrderShipped event occurred.”
That distinction matters because the event does not merely describe state; it describes a fact. Facts do not get overwritten.
This has several benefits:
You keep full historical context.
You can answer “how did we get here?”
You can rebuild derived state if your projection logic changes.
You can support multiple views of the same data.
But this also means your system is no longer just “storing data.” It is now preserving a history that must remain meaningful over time.
And that is where the fun begins.
Replays: The Superpower and the Footgun
Replays are what make event sourcing truly useful.
A replay means taking the stored events and applying them again, usually to:
rebuild projections after a bug fix,
populate a new read model,
recover system state after a failure,
test event handler behavior,
or run analytics against the historical stream.
This is fantastic, because it gives you a way to reconstruct the system at will.
But replays also define an operational contract. Once you rely on replay, your handlers must be deterministic. Given the same sequence of events, they should always produce the same result.
That means:
no hidden randomness,
no dependence on changing external state,
no logic that behaves differently depending on the day of the week unless that’s explicit domain behavior,
and careful handling of idempotency.
If replaying the same events twice changes the result, your architecture has started lying to you.
Why determinism matters
Imagine a projection that counts orders and accidentally increments twice when the same event is replayed. Now your “truth” is inflated. That’s not a small bug. That’s a history problem.
Why idempotency matters
Events may be delivered more than once. Systems fail. Consumers retry. Queues do queue-like things. Your event handlers need to be resilient to duplicate processing.
Why stable semantics matter
If you change the meaning of an event without versioning it properly, replays can silently become incorrect. That is one of event sourcing’s cruelest jokes: the system works until you try to understand the past.
In plain terms: replay is not “just re-reading the log.” It is a carefully maintained pipeline that must be testable, versioned, and disciplined.
The Big Trade-Off: Simpler Writes, Harder Everything Else
Event sourcing often simplifies write-time persistence. Instead of updating multiple tables or chasing denormalized state, you append one event. That is clean and elegant.
But the cost moves elsewhere.
Debugging becomes more layered
When a user says, “The current view is wrong,” the root cause may be in:
the event itself,
the projection logic,
the replay process,
schema evolution,
a handler bug,
or a missed edge case in versioning.
With a traditional mutable row, the problem is often localized. With event sourcing, the issue may be in any one of several stages.
Schema evolution is harder
Events are not just data. They are historical contracts. Once emitted, they can live for years.
So if your domain changes, you must decide how old events continue to make sense.
Typical strategies include:
versioned event types,
upcasters,
backward-compatible payload evolution,
and migration layers in replay pipelines.
Mental model complexity increases
Developers must think in terms of:
commands,
domain events,
projections,
snapshots,
subscriptions,
eventual consistency,
and reprocessing semantics.
That’s a lot more than “update row, commit transaction, go home and pretend distributed systems are someone else’s problem.”
Operational complexity rises
You need monitoring for consumers, replay jobs, lag, projection correctness, storage growth, and version compatibility.
So the major trade-off is this:
event sourcing can make writing data elegant, but it makes the overall system architecture more demanding.
Python Example: A Tiny Event-Sourced Aggregate
Here’s a simple example in Python to show the pattern.
from dataclasses import dataclass, asdict
from typing import List, Union
# Domain Events
@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]
class BankAccount:
def __init__(self):
self.account_id = None
self.balance = 0
self._uncommitted_events: List[Event] = []
def apply(self, event: Event):
if isinstance(event, AccountOpened):
self.account_id = event.account_id
self.balance = event.initial_balance
elif isinstance(event, MoneyDeposited):
self.balance += event.amount
elif isinstance(event, MoneyWithdrawn):
self.balance -= event.amount
def open_account(self, account_id: str, initial_balance: int):
event = AccountOpened(account_id, initial_balance)
self.apply(event)
self._uncommitted_events.append(event)
def deposit(self, amount: int):
event = MoneyDeposited(self.account_id, amount)
self.apply(event)
self._uncommitted_events.append(event)
def withdraw(self, amount: int):
if self.balance < amount:
raise ValueError("Insufficient funds")
event = MoneyWithdrawn(self.account_id, amount)
self.apply(event)
self._uncommitted_events.append(event)
@classmethod
def rehydrate(cls, events: List[Event]):
account = cls()
for event in events:
account.apply(event)
return account
# Example usage
history = [
AccountOpened("acct-123", 100),
MoneyDeposited("acct-123", 50),
MoneyWithdrawn("acct-123", 20),
]
account = BankAccount.rehydrate(history)
print(account.balance) # 130This example captures the essence of event sourcing:
events are immutable facts,
state is reconstructed by replay,
and the aggregate behaves as a pure function of its history.
Of course, real systems also need persistence, concurrency control, versioning, and projections. Because if backend engineering were allowed to stay this simple, we’d all be sleeping more.
Projections: How You Make Reads Fast
If the event log is your truth, projections are your convenience.
A projection is a read-optimized view built from events. For example:
a customer summary table,
an order status dashboard,
a search index,
a reporting warehouse,
or a materialized view for querying by status.
Why projections matter:
Replaying a huge event stream for every user request would be painfully slow.
Most systems need fast reads.
Different consumers need different shapes of data.
So instead of querying the event log directly every time, you build one or more projections.
This is one of the most important practical lessons in event sourcing:
the raw event log alone is not enough.
You need read models.
And once you have read models, you’ve introduced eventual consistency. The write side changes first, and the projection catches up later. That’s acceptable in many domains, but it is a trade-off you must consciously embrace.
Snapshots: Reducing the Cost of Rebuilding History
Replaying millions of events to rebuild a state object can become expensive.
That is where snapshots come in.
A snapshot is a saved state at a particular point in time. Instead of replaying from the beginning of history, you start from the snapshot and apply only the events that happened after it.
This helps with:
faster rehydration,
shorter recovery times,
more efficient replays,
and better performance in large aggregates.
Important note: snapshots are an optimization, not the source of truth. The event log still is.
When to use snapshots
Snapshots are useful when:
aggregate histories are long,
rehydration is expensive,
or replaying from zero becomes too slow.
What snapshots do not solve
Snapshots do not eliminate the need for versioning, deterministic handlers, or replay-safe logic. They just reduce the amount of history you need to process in one go.
Think of them as bookmarks in a very long and highly opinionated novel.
Event Versioning and Schema Evolution
This is where many event sourcing systems grow a few gray hairs.
Your domain will evolve. It always does. Product managers will discover new ideas. Compliance will demand a new field. Marketing will ask for “just one more attribute.” Someone, somewhere, will use the phrase “should be quick.”
But old events still exist.
So you need a strategy for evolution:
Additive changes
The safest route is often adding new optional fields while keeping old ones intact.
New event versions
You may define OrderPlacedV2 instead of changing OrderPlaced in place.
Upcasting
You transform older event payloads into newer structures during replay.
Translation layers
You convert old events into new domain shapes as they are read.
The important idea is this: events are long-lived contracts.
You are not just designing a current data model. You are designing historical semantics.
That’s why event sourcing rewards teams that treat event definitions like APIs, not like temporary implementation details.
When Event Sourcing Works Best
Event sourcing is not a universal default. It shines in specific kinds of systems.
It tends to work well when:
the domain history is valuable,
auditability is required,
disputes or investigations are common,
projections can be eventually consistent,
and the bounded context is clear.
Examples:
financial ledgers,
order management,
inventory movement,
booking systems,
approval workflows,
compliance systems,
and collaborative systems where history matters.
It is less attractive when:
the data is simple CRUD,
historical reconstruction is not valuable,
team maturity is low,
or operational overhead needs to stay minimal.
A lot of backend architectures fail because people adopt them for ideology instead of fit. Event sourcing is a specialized tool. A very sharp one. Not a hammer for every nail.
Real-World Tooling Makes All the Difference
In production, event sourcing is rarely built from scratch end-to-end. Most teams lean on established ecosystem support.
Examples include:
EventStoreDB for event storage and subscriptions,
Axon for CQRS and event-driven architecture support,
Marten for event storage on PostgreSQL,
NEventStore in .NET ecosystems,
Kafka-based patterns for distributed event streams,
and Python libraries like
eventsourcing.
These tools help with:
storage,
subscriptions,
replay,
projections,
and operational primitives.
This matters because event sourcing is not just a pattern. It is a system of responsibilities. Good tooling reduces the surface area enough that the approach becomes viable in real production environments.
Without tooling, you’re basically building a very expensive log-based philosophy project.
Practical Guidelines if You Want to Use It
If you’re considering event sourcing, here are the rules I’d put on the wall in bold marker:
Start with the domain, not the pattern.
If history matters, event sourcing may fit. If not, don’t force it.Treat events as immutable contracts.
Version them carefully and keep semantics stable.Design projections as first-class citizens.
Fast reads don’t happen by magic.Make replay a tested path.
Don’t assume reprocessing will “just work.”Use snapshots where replay cost becomes painful.
Build idempotency into consumers.
Duplicate processing will happen eventually.Monitor lag and projection correctness.
A healthy event store with broken projections is still a broken system.Expect operational maturity to matter.
This pattern rewards teams that test, version, and observe carefully.
A Tiny Mental Model to Remember
If traditional persistence asks:
“What is the state right now?”
Event sourcing asks:
“What sequence of facts created this state?”
That shift is beautiful when you need it.
It is also the reason event sourcing can feel like explaining a joke to a compiler: conceptually elegant, operationally exacting, and not forgiving when you cut corners.
References to Libraries and Services Worth Exploring
If you want to go deeper, these are commonly used in real systems:
EventStoreDB — purpose-built event storage and streaming
Axon Framework — popular in Java for CQRS and event sourcing
Marten — PostgreSQL-backed document and event store for .NET
NEventStore — event store abstraction in .NET ecosystems
Apache Kafka — often used for event streaming and replay-centric architectures
eventsourcing— a Python library for event-sourced applications
Each of these brings different trade-offs, especially around storage model, replay ergonomics, projection handling, and operational complexity.
Closing Thoughts
Event sourcing is one of those ideas that makes engineers feel both brilliant and slightly under-caffeinated.
Used well, it gives you auditable history, time-travel debugging, reconstructable state, and a powerful foundation for replayable systems. Used carelessly, it gives you a beautiful log and a confusing pile of projections that all disagree with each other.
The research is clear: the architecture pays off when the domain truly needs history, traceability, and recovery. It also demands discipline in replay handling, versioning, snapshots, and operational design. That’s the deal.
So if your backend needs to answer not just “what is true now?” but “how did we get here, and can we prove it?”, event sourcing is worth serious consideration.
If you enjoyed this breakdown, come back tomorrow with your coffee, your curiosity, and your healthiest skepticism.
Until then, keep your events immutable and your projections honest.
Warmly,
The Backend Developers









