Most applications store the current state of their data.
For example, an order might be stored like this:
orders
+----+----------+---------+--------+
| id | customer | status | total |
+----+----------+---------+--------+
| 42 | 1001 | PAID | 250.00 |
+----+----------+---------+--------+
The database tells us:
Order 42 is PAID
Order 42 belongs to customer 1001
Order 42 costs $250
But it doesn't tell us much about how the order got there.
Perhaps:
OrderCreated
ItemAdded
ItemAdded
DiscountApplied
PaymentInitiated
PaymentCompleted
OrderPaid
A traditional CRUD system normally stores the result:
Order 42
status = PAID
total = 250
An Event Sourcing system stores the history:
OrderCreated
ItemAdded
ItemAdded
DiscountApplied
PaymentInitiated
PaymentCompleted
OrderPaid
The current state can then be reconstructed from those events.
That is the fundamental idea behind Event Sourcing.
Instead of treating the current state as the primary source of truth, we treat the sequence of events that produced the state as the source of truth.
Microsoft describes Event Sourcing as storing a full series of actions in an append-only store and using those events to materialize domain objects.
The Problem With Storing Only Current State
Consider a bank account.
A traditional database might contain:
account_id = 1001
balance = 750
That tells us the current balance.
But suppose the account originally had:
1000
Then:
-100 ATM withdrawal
+500 salary
-250 payment
-400 transfer
The final state is:
750
The database may only contain:
balance = 750
The history is gone unless we explicitly maintain an audit table.
This creates several problems.
1. Auditability
We may need to answer:
Why is the balance $750?
A current-state database cannot answer this by itself.
2. Debugging
Suppose an order unexpectedly becomes:
CANCELLED
We want to know:
Who cancelled it?
When?
Why?
What happened before cancellation?
3. Historical reconstruction
We may want to know:
What was the order status at 10:30 AM yesterday?
A current-state table doesn't contain that information.
4. New requirements
Imagine that six months after building the system, the business asks:
We want a report showing how many orders moved from
PENDINGtoPAIDwithin 10 minutes.
If the database only contains current state, reconstructing that information may be impossible.
With an event history, it becomes a projection problem.
The Traditional CRUD Model
The traditional model looks like this:
Application
|
v
+-----------+
| Order |
+-----------+
|
v
UPDATE orders
|
v
+-----------+
| Database |
+-----------+
Suppose:
UPDATE orders
SET status = 'PAID'
WHERE id = 42;
The old value:
PENDING
is replaced by:
PAID
The database primarily represents:
current state
The Event Sourcing Model
With Event Sourcing:
Application
|
v
Command Handler
|
v
Domain Model
|
v
New Event
|
v
+-----------+
| Event |
| Store |
+-----------+
|
+----------+----------+
| | |
v v v
Read DB Search Analytics
Instead of:
UPDATE orders
SET status = 'PAID'
WHERE id = 42;
we append:
OrderPaid
The event is not an update to the previous event.
It is a new fact.
What Is an Event?
An event represents something that has already happened.
Examples:
OrderCreated
ItemAddedToOrder
PaymentAuthorized
PaymentCaptured
OrderShipped
OrderDelivered
OrderCancelled
Notice the naming.
Events normally describe facts:
OrderCreated
rather than commands:
CreateOrder
A command means:
Please do this.
An event means:
This happened.
That distinction is extremely important.
Command vs Event
Consider:
PayOrder
This is a command.
It represents an intention:
Please pay this order.
After the business logic successfully processes the command, it might produce:
OrderPaymentCompleted
That is an event.
The flow is:
Command
|
v
PayOrder
|
v
Business Rules
|
v
OrderPaymentCompleted
Commands can fail.
Events represent something that has already occurred.
The Event Store
The central component of Event Sourcing is the event store.
An event store is an append-oriented storage system containing events.
A simplified schema could look like:
CREATE TABLE events (
event_id UUID PRIMARY KEY,
aggregate_id UUID NOT NULL,
aggregate_type VARCHAR(100) NOT NULL,
version BIGINT NOT NULL,
event_type VARCHAR(200) NOT NULL,
payload JSONB NOT NULL,
occurred_at TIMESTAMP NOT NULL
);
Example:
+--------------------------------------+
| Event Store |
+--------------------------------------+
| aggregate_id | version | event |
+--------------------------------------+
| order-42 | 1 | OrderCreated|
| order-42 | 2 | ItemAdded |
| order-42 | 3 | ItemAdded |
| order-42 | 4 | Discounted |
| order-42 | 5 | OrderPaid |
+--------------------------------------+
The important property is:
Events are appended.
Existing events are not updated.
This gives us an immutable history.
Event Stream
Events normally belong to an entity or aggregate.
For example:
Order 42
may have the stream:
OrderCreated
|
v
ItemAdded
|
v
ItemAdded
|
v
DiscountApplied
|
v
PaymentCompleted
|
v
OrderShipped
This ordered sequence is the event stream for that aggregate.
The stream represents the complete history of the aggregate.
A typical event might contain:
{
"eventId": "e123",
"aggregateId": "order-42",
"version": 5,
"type": "OrderPaid",
"occurredAt": "2026-09-07T10:30:00Z",
"data": {
"paymentId": "payment-99",
"amount": 250
}
}
The Event Stream Is the Source of Truth
This is the most important conceptual change.
In CRUD:
Database row
|
v
Current state
In Event Sourcing:
Event Stream
|
v
Current state
For example:
OrderCreated
+
ItemAdded
+
ItemAdded
+
DiscountApplied
+
OrderPaid
|
v
Current Order
The current order is a derived state.
The event history is the authoritative state.
Rehydration
How do we obtain the current order?
We replay the events.
Suppose:
OrderCreated
ItemAdded(product=A, quantity=2)
ItemAdded(product=B, quantity=1)
DiscountApplied(20)
OrderPaid
The application starts with:
Order order = new Order();
Then:
apply(OrderCreated)
|
v
empty order created
apply(ItemAdded)
|
v
2 × product A
apply(ItemAdded)
|
v
2 × product A
1 × product B
apply(DiscountApplied)
|
v
discount = 20
apply(OrderPaid)
|
v
status = PAID
The resulting object represents the current state.
This process is called rehydration.
Event Replay
Conceptually:
Order order = Order.empty();
for (Event event : eventStore.load(orderId)) {
order.apply(event);
}
The important property is:
State = initial state + all events
Therefore:
Events
|
v
Replay
|
v
Current State
This is one of the most powerful capabilities of Event Sourcing.
Why Replay Matters
Suppose an order has this history:
v1 OrderCreated
v2 ItemAdded
v3 ItemAdded
v4 DiscountApplied
v5 PaymentCompleted
v6 OrderShipped
We can replay everything:
v1 → v2 → v3 → v4 → v5 → v6
and obtain the current state.
But we can also replay only:
v1 → v2 → v3
to determine the state at that point.
Therefore Event Sourcing provides a natural way to answer:
What was the state at version 3?
or:
What was the state at 10:30 AM?
provided the event history and timestamps allow that reconstruction.
Event Sourcing Is Not Event Logging
This distinction is critical.
A normal application might write:
INFO Order 42 changed status from PENDING to PAID
into a log file.
That is not Event Sourcing.
The log is informational.
The application still treats the database as the source of truth.
With Event Sourcing:
Event Store
|
v
Source of Truth
The events themselves are the authoritative representation of the domain history.
Event Sourcing Is Not Just an Audit Table
Another common misconception is:
We already have an audit table, so we are using Event Sourcing.
Not necessarily.
An audit table might contain:
entity_id
old_value
new_value
changed_by
changed_at
while the real system still operates on:
orders
customers
payments
The audit table is secondary.
In Event Sourcing:
Events
|
+--> Current State
+--> Read Models
+--> Reports
+--> Search Indexes
+--> Integrations
The event store is the authoritative source.
State-Based Events vs Intent-Based Events
Event design is one of the most important parts of Event Sourcing.
Consider:
BalanceChanged
with:
{
"balance": 750
}
This tells us the result.
But it doesn't tell us why the balance changed.
A better event might be:
MoneyDeposited
with:
{
"amount": 500,
"source": "SALARY"
}
Another:
MoneyWithdrawn
with:
{
"amount": 250,
"channel": "ATM"
}
Now the history contains business meaning.
Microsoft specifically recommends events that capture business intent rather than merely recording resulting state changes.
Prefer:
SeatReserved
over:
AvailableSeatsChanged
Prefer:
OrderCancelled
over:
OrderStatusChanged
Prefer:
PaymentCaptured
over:
PaymentStatusChanged
The first versions preserve meaning.
Events Should Be Facts
An event should describe something that happened.
Bad:
ChangeOrderStatus
Good:
OrderCancelled
Bad:
UpdateCustomer
Good:
CustomerAddressChanged
Bad:
SetBalance
Good:
MoneyDeposited
This makes event histories understandable to both developers and domain experts.
The Aggregate
Event Sourcing is commonly used with Domain-Driven Design.
An aggregate represents a consistency boundary.
For example:
Order
could be an aggregate.
Its event stream:
OrderCreated
ItemAdded
ItemRemoved
DiscountApplied
OrderPaid
OrderShipped
The aggregate:
Order
receives commands.
For example:
PayOrder
It loads its current state from events:
Event Stream
|
v
Order
|
v
Business Rules
|
v
New Events
Command Processing
Suppose we receive:
ShipOrder(orderId)
The command handler does something like:
1. Load Order events
2. Replay events
3. Reconstruct Order
4. Execute ShipOrder
5. Validate business rules
6. Generate OrderShipped
7. Append event
Diagram:
ShipOrder
|
v
+--------------+
| Event Store |
+--------------+
|
| load stream
v
+--------------+
| Order |
| Aggregate |
+--------------+
|
| execute command
v
Business Rules
|
v
OrderShipped
|
v
Append Event
Event Sourcing and CQRS
Event Sourcing and CQRS are related, but they are not the same pattern.
CQRS says:
Separate writes from reads.
Event Sourcing says:
Store state changes as an immutable sequence of events.
You can have:
CQRS without Event Sourcing
and:
Event Sourcing without full CQRS
But they work extremely well together.
A common architecture is:
Commands
|
v
+---------------+
| Command Side |
+---------------+
|
v
Event Store
|
+-----+------+
| |
v v
Projection Integration
|
v
Read Store
|
v
Queries
Microsoft describes this combination as using the event store as the write model and materialized views as the read model.
CQRS Without Event Sourcing
CQRS can simply be:
Command
|
v
Write Database
and:
Query
|
v
Read Database
The write database might still contain normal CRUD tables.
Therefore:
CQRS ≠ Event Sourcing
Event Sourcing Without CQRS
You could have:
Application
|
v
Event Store
|
v
Replay
|
v
Current State
without completely separating query and command models.
However, once the event history becomes the source of truth, read models are often introduced because directly replaying events for every query is inefficient.
Why We Need Projections
Suppose an order contains:
500 events
Now imagine the UI asks:
GET /orders/42
We could replay all 500 events.
That works.
But suppose we have:
1 million orders
and thousands of queries per second.
Replaying event streams for every query is expensive.
Instead, we build a projection.
Event Store
|
v
Projection Handler
|
v
+----------------+
| orders_read |
+----------------+
The projection might store:
order_id
customer_name
status
total
shipping_address
updated_at
Now:
SELECT *
FROM orders_read
WHERE order_id = 42;
is fast.
Materialized Read Models
A projection is essentially a materialized representation derived from events.
For example:
Events
|
+--> Order Summary
|
+--> Customer Order History
|
+--> Sales Report
|
+--> Search Index
|
+--> Analytics
One event stream can therefore produce many different views.
This is one of the strongest reasons to use Event Sourcing.
One Event Stream, Many Views
Suppose we have:
OrderCreated
ItemAdded
PaymentCaptured
OrderShipped
OrderDelivered
We can create:
orders_read
for the customer UI.
We can create:
sales_report
for finance.
We can create:
shipping_view
for logistics.
We can create:
customer_order_history
for customer support.
All of them originate from the same event history.
Event Store
|
+----------------+----------------+
| | |
v v v
Order View Sales View Shipping View
AWS highlights this ability to create multiple projections from a single source of truth as a major use case for Event Sourcing.
Projections Are Disposable
This leads to an important architectural idea.
The event store is authoritative.
The projection is derived.
Therefore:
Projection
|
v
Can be deleted
|
v
Rebuild from events
Suppose:
orders_read
becomes corrupted.
We don't necessarily need to restore the database from a backup.
We can:
1. Delete orders_read
2. Start projection from beginning
3. Replay events
4. Rebuild orders_read
This is extremely powerful.
Projection Replay
Suppose the event store contains:
1 OrderCreated
2 ItemAdded
3 ItemAdded
4 DiscountApplied
5 PaymentCompleted
The projection processes:
Event 1 → create row
Event 2 → update row
Event 3 → update row
Event 4 → update row
Event 5 → update row
Eventually:
orders_read
contains the current state.
Eventual Consistency
Once projections are asynchronous, something important happens.
Suppose:
POST /orders/42/pay
returns successfully.
The event:
OrderPaid
has been stored.
But the read projection may not have processed it yet.
Therefore:
Event Store
|
| OrderPaid
v
Read Model
|
| processing...
v
Updated Read Model
For a short period:
Write side = PAID
Read side = PENDING
This is eventual consistency.
Event Sourcing combined with asynchronous projections commonly introduces this behavior.
Read-After-Write Problem
Consider:
POST /orders/42/pay
followed immediately by:
GET /orders/42
The client might see:
status = PENDING
even though the payment command succeeded.
This can surprise developers.
The system is not necessarily broken.
The read model has simply not caught up.
Possible solutions include:
1. Return the updated state from the command
2. Read from the write model when necessary
3. Track projection version
4. Wait until projection reaches a required version
5. Use synchronous projection for critical paths
The correct solution depends on the consistency requirements.
Event Store vs Message Broker
This is another important distinction.
An event store is:
system of record
A message broker is:
distribution mechanism
For example:
Event Store
|
v
Kafka
|
+--> Projection
+--> Search
+--> Notifications
+--> Analytics
Kafka can distribute events.
But Kafka should not automatically be assumed to be the event store.
An event store typically needs capabilities such as:
Load stream by aggregate ID
Append to stream
Check expected version
Optimistic concurrency
Replay stream
Snapshots
Microsoft explicitly notes that message brokers such as Kafka are useful for distributing events but are not automatically substitutes for an event store.
Optimistic Concurrency
Concurrency is one of the hardest parts of Event Sourcing.
Suppose an order currently has:
version = 10
Two requests arrive simultaneously:
Request A
Request B
Both load:
version = 10
Then:
A → OrderPaid
B → OrderCancelled
If both events are appended blindly:
v11 OrderPaid
v12 OrderCancelled
we may have an invalid history.
The solution is optimistic concurrency.
The command says:
Append this event only if
current version == 10
Request A succeeds:
version 10 → 11
Request B then attempts:
expected version = 10
actual version = 11
The event store rejects it.
Concurrency conflict
The command handler can then reload the aggregate and reevaluate the command.
This optimistic version-checking model is a standard consideration in Event Sourcing systems.
Version Numbers
A stream might look like:
aggregate_id = order-42
version 1 → OrderCreated
version 2 → ItemAdded
version 3 → ItemAdded
version 4 → DiscountApplied
version 5 → PaymentCompleted
The version provides:
Ordering
Concurrency control
Stream position
A common append operation is conceptually:
append(
aggregateId = order-42,
expectedVersion = 4,
event = PaymentCompleted
)
If the actual version is not 4, reject the operation.
Event Ordering
Ordering matters.
Consider:
PaymentCompleted
OrderCancelled
versus:
OrderCancelled
PaymentCompleted
These histories may produce completely different results.
For an individual aggregate:
Event 1
Event 2
Event 3
...
must have deterministic ordering.
Global ordering across the entire system is usually unnecessary.
What normally matters is:
ordering within an aggregate stream
Snapshots
Replay is powerful, but an aggregate might eventually have:
1,000,000 events
Replaying all one million events every time is expensive.
Snapshots solve this problem.
Suppose:
Events 1 ... 10,000
produce:
Snapshot at version 10,000
Later:
Events 10,001
Events 10,002
Events 10,003
To rebuild the aggregate:
Snapshot v10,000
+
Events v10,001-v10,003
|
v
Current State
Instead of:
Replay 10,003 events
we replay:
3 events
Snapshots Are Not the Source of Truth
This distinction is important.
The event store contains:
Source of truth
The snapshot is:
Optimization
If the snapshot disappears:
Replay events
and rebuild it.
Therefore:
Events
|
+--> Snapshot
|
+--> Read Models
The snapshot is derived data.
Event Versioning
Events live for a long time.
Imagine today's event:
{
"type": "CustomerAddressChanged",
"data": {
"street": "Main Street",
"city": "Kathmandu"
}
}
Five years later the application expects:
{
"type": "CustomerAddressChanged",
"data": {
"line1": "Main Street",
"city": "Kathmandu",
"country": "Nepal"
}
}
What happens to old events?
You cannot casually change them.
The event history is supposed to be immutable.
Event Schema Evolution
Common approaches include:
Upcasting
Transform an old event into the current representation while reading it.
Old Event
|
v
Upcaster
|
v
Current Event Model
Multiple Event Versions
For example:
CustomerAddressChangedV1
CustomerAddressChangedV2
The application knows how to handle both.
New Event Types
Instead of modifying an old event:
CustomerAddressChanged
introduce:
CustomerAddressChangedWithCountry
The important principle is:
Don't silently rewrite history.
Why Immutable Events Matter
Suppose the original event was:
MoneyDeposited
amount = 1000
Changing it later to:
amount = 2000
changes history.
Now the event store no longer tells us what actually happened.
Instead, if another correction is necessary, append another event:
MoneyDeposited 1000
CorrectionApplied -500
The history remains intact.
Event Sourcing Does Not Mean "Never Reverse Anything"
Suppose:
MoneyDeposited 1000
was incorrect.
You don't update the original event.
Instead:
MoneyDeposited 1000
DepositReversed 1000
The current state becomes:
0
but the history still tells us:
Money was deposited.
Then that deposit was reversed.
This is much more informative than silently modifying the original record.
Deleting Data
This creates a difficult question:
What happens when a user asks us to delete their data?
Immutable event histories can conflict with privacy and regulatory requirements.
You may need strategies such as:
PII encryption
Key destruction
Data minimization
Redaction strategies
Separate sensitive-data storage
Cryptographic erasure
The exact solution depends heavily on the legal and regulatory requirements.
Event Sourcing should therefore not be adopted without considering data-retention requirements.
Event Store Schema
A relational implementation might look like:
CREATE TABLE event_store (
event_id UUID PRIMARY KEY,
aggregate_id UUID NOT NULL,
aggregate_type VARCHAR(100) NOT NULL,
version BIGINT NOT NULL,
event_type VARCHAR(200) NOT NULL,
payload JSONB NOT NULL,
metadata JSONB,
occurred_at TIMESTAMP NOT NULL,
UNIQUE (aggregate_id, version)
);
The unique constraint:
(aggregate_id, version)
helps guarantee that an aggregate cannot have two events at the same version.
Why Append-Only Storage Helps
Traditional updates look like:
Read row
|
Modify row
|
Write row
This can create contention.
Event Sourcing instead performs:
Append event
The append-only nature can reduce update contention, particularly when writes are naturally partitioned by aggregate.
But this does not mean Event Sourcing automatically makes every system faster.
The trade-offs depend on workload, event volume, storage, projections, and aggregate design.
Event Sourcing and Transactions
Suppose a command generates:
OrderPaid
PaymentRecorded
If both events belong to the same aggregate transaction, they should normally be appended atomically.
We want:
Append event A
Append event B
to behave as:
SUCCESS
or:
FAIL
rather than:
A succeeded
B failed
For cross-aggregate or cross-service operations, however, Event Sourcing does not magically provide distributed transactions.
That is where patterns such as:
Saga
Outbox
Idempotency
become important.
Event Sourcing and Transactional Outbox
The Transactional Outbox solves:
Database update
+
Message publication
when both need reliable coordination.
Event Sourcing changes the model.
The event store itself is already the authoritative persistence mechanism for domain events.
A common architecture becomes:
Command
|
v
Aggregate
|
v
Event Store
|
v
Event Publication
|
v
Consumers
Depending on the technology, the event store can provide a durable event log from which downstream consumers can receive or replay events.
The important point is that Event Sourcing and Transactional Outbox solve different problems, although an implementation can use both.
Event Sourcing + CQRS + Outbox
A production system may combine all three.
Commands
|
v
+--------------+
| Command Side |
+--------------+
|
v
+--------------+
| Event Store |
+--------------+
|
v
Outbox / Log
|
+--------+--------+
| | |
v v v
Read Model Search Integration
|
v
Query API
Each pattern has a different responsibility:
CQRS
Separates reads and writes
Event Sourcing
Makes event history the source of truth
Transactional Outbox
Reliably coordinates DB state and message publication
Do not treat them as interchangeable patterns.
Event Sourcing + Saga
Now consider an order workflow:
Order
|
v
Payment
|
v
Inventory
|
v
Shipping
Each service might have its own event history.
For example:
Order Service
|
| OrderPlaced
v
Payment Service
|
| PaymentCompleted
v
Inventory Service
|
| InventoryReserved
v
Shipping Service
If inventory reservation fails:
InventoryReservationFailed
the Saga can trigger:
RefundPayment
which produces:
PaymentRefunded
Event Sourcing records the history.
Saga coordinates the distributed business transaction.
These are different responsibilities.
Rebuilding a Read Model
One of the strongest operational capabilities of Event Sourcing is rebuilding projections.
Suppose:
orders_read
was accidentally deleted.
We can create it again:
Event Store
|
v
Replay
|
v
orders_read
Or perhaps the business introduces a new requirement:
Show the average time between order creation and payment.
We don't necessarily need to modify the command side.
We can create a new projection:
OrderCreated
|
OrderPaid
|
v
PaymentTimingProjection
The historical events already exist.
Replaying Historical Events
Imagine a new projection:
customer_lifetime_value
We can:
1. Create empty projection
2. Read historical events
3. Process events
4. Calculate customer totals
5. Continue consuming new events
This is one of the biggest advantages of Event Sourcing.
Historical data becomes an input to new models.
What-If Analysis
Because the history is preserved, we can sometimes replay it under different rules.
For example:
Original events
|
v
Original projection
and:
Original events
|
v
New business rules
|
v
Alternative projection
This can support:
What-if analysis
Historical reporting
New business models
Data migrations
Reprocessing
AWS lists replay and point-in-time reconstruction among the major applicability scenarios for Event Sourcing.
Projection Failure
Suppose:
Event Store
|
v
OrderProjection
and the projection crashes while processing:
OrderPaid
The event is still safe in the event store.
The projection can retry.
This is an important architectural property:
Projection failure
|
v
Derived data unavailable
|
v
Replay / retry
|
v
Projection recovered
The source of truth has not been lost.
Idempotent Projections
But retries create another problem.
Suppose the projection processes:
OrderPaid
and updates:
orders_read
Then it crashes before recording that the event was processed.
The event is delivered again.
Now:
OrderPaid
OrderPaid
may be processed twice.
Therefore projections should generally be idempotent.
For example:
processed_event_id
can be tracked.
if alreadyProcessed(event.id):
return;
apply(event);
markProcessed(event.id);
Or the projection operation itself can be designed to be naturally idempotent.
Poison Events
What happens if one historical event is malformed?
Suppose:
v100
v101
v102 <-- malformed
v103
Replay reaches:
v102
and fails.
The projection cannot simply continue blindly.
Possible strategies include:
Dead-letter handling
Manual investigation
Event upcasting
Projection-specific error handling
Skipping only when safe
Correcting source data through an explicit process
The event store is history, so mistakes in event design can become long-lived operational problems.
Projection Lag
In an asynchronous architecture, monitor:
event store position
-
projection position
For example:
Event Store: 1,000,000
Projection: 999,850
Lag:
150 events
Important metrics include:
Projection lag
Event processing rate
Event processing failures
Retry count
Dead-letter count
Replay duration
Event-store append latency
Stream length
Snapshot age
Without these metrics, eventual consistency becomes very difficult to operate.
Event Store Growth
Event stores grow continuously.
Traditional CRUD:
UPDATE orders
may keep the database relatively small.
Event Sourcing:
OrderCreated
ItemAdded
ItemRemoved
ItemAdded
DiscountApplied
PaymentCompleted
...
creates a permanent history.
Therefore you need to consider:
Storage growth
Partitioning
Archival
Compression
Retention requirements
Snapshotting
Cold storage
However, deleting historical events is not always acceptable because the history itself is the source of truth.
Snapshots vs Archiving
These solve different problems.
Snapshot:
Reduce replay cost
Archiving:
Reduce hot storage requirements
For example:
Event Store
|
+-- Recent events
|
+-- Archived events
|
+-- Snapshot
The exact strategy depends on whether historical replay must remain immediately available.
Event Store Partitioning
Event streams naturally provide a useful partitioning key:
aggregate_id
For example:
order-1
order-2
order-3
...
Events for different aggregates can often be processed independently.
Partition A
order-1
order-7
order-12
Partition B
order-2
order-8
order-15
But events belonging to the same aggregate must preserve their required ordering.
Event Sourcing in Java
A simplified aggregate might look like:
public class Order {
private OrderStatus status;
private BigDecimal total;
public void handle(PayOrder command) {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException(
"Order cannot be paid"
);
}
raise(new OrderPaid(command.paymentId()));
}
public void apply(OrderPaid event) {
this.status = OrderStatus.PAID;
}
}
The aggregate has two important concepts:
handle(command)
and:
apply(event)
The first contains business decisions.
The second reconstructs state.
Command Handling
Conceptually:
Order order = repository.load(orderId);
List<Event> newEvents =
order.handle(command);
repository.append(
orderId,
order.version(),
newEvents
);
The repository:
load events
|
v
rehydrate aggregate
|
v
execute command
|
v
append new events
Applying Events
A clean model keeps event application deterministic.
For example:
private void apply(OrderCreated event) {
this.id = event.orderId();
this.status = OrderStatus.PENDING;
}
private void apply(ItemAdded event) {
this.items.add(
new OrderItem(
event.productId(),
event.quantity()
)
);
}
private void apply(OrderPaid event) {
this.status = OrderStatus.PAID;
}
The aggregate's current state becomes a function of its event history.
Event Handler vs Event Application
These are often confused.
Inside an aggregate:
OrderPaid
|
v
apply OrderPaid
|
v
Change Order state
A separate event handler might do:
OrderPaid
|
+--> Update read model
+--> Send notification
+--> Update search index
+--> Publish integration event
These have different responsibilities.
Domain Events vs Integration Events
Not every internal event should automatically become an external integration event.
An internal event might be:
OrderDiscountCalculated
An external consumer might only need:
OrderConfirmed
Therefore:
Domain Event
|
v
Integration Event
may involve transformation.
This also prevents internal implementation details from becoming external contracts.
Event Contracts Are APIs
Once other systems consume an event:
OrderPaid
its schema becomes a contract.
Changing:
{
"amount": 100
}
to:
{
"paymentAmount": 100
}
can break consumers.
Therefore events require:
Schema versioning
Backward compatibility
Documentation
Ownership
Validation
Contract testing
Treat important events with the same care as public APIs.
Event Sourcing and Database Transactions
One misconception is:
Event Sourcing eliminates transactions.
It doesn't.
We still need atomicity within the consistency boundary.
For example:
Validate command
+
Append events
must be atomic.
The important difference is what is being persisted.
Traditional:
UPDATE current state
Event Sourcing:
APPEND new facts
Event Sourcing and Strong Consistency
Event Sourcing does not automatically mean:
Everything is eventually consistent.
The event store can provide strong consistency for a single aggregate stream.
For example:
Expected version = 10
ensures that concurrent modifications are detected.
Eventual consistency typically appears when:
Events
|
v
Asynchronous projections
are used.
Therefore distinguish:
Aggregate consistency
from:
Read-model consistency
What Event Sourcing Gives You
The biggest benefits are:
Complete history
What happened?
Auditability
Who did what?
Reconstruction
What was the state at a particular point?
Replay
Can we rebuild this model?
Multiple projections
Can we create another view?
Temporal analysis
How did the state evolve?
Decoupling
Can multiple consumers react independently?
These benefits are particularly valuable in domains where the history of changes is itself important.
What Event Sourcing Costs
Event Sourcing also introduces significant complexity.
1. Event schema evolution
Events live for a long time.
2. Replay complexity
The application must be able to replay historical events correctly.
3. Projection management
Read models can fail, lag, or become inconsistent temporarily.
4. Storage growth
Events accumulate.
5. Debugging complexity
A bug may involve:
Command
→ Event
→ Projection
→ Consumer
rather than a single database transaction.
6. Data privacy
Immutable history complicates deletion and redaction.
7. Operational complexity
You now need to operate:
Event store
Projections
Replay mechanisms
Snapshots
Event consumers
Monitoring
Microsoft explicitly warns that Event Sourcing is a complex pattern and should be adopted only when benefits such as auditability and historical reconstruction justify the additional complexity.
When Event Sourcing Is a Good Fit
Event Sourcing is particularly attractive when:
History matters
For example:
Financial transactions
Payment systems
Trading systems
Order workflows
Booking systems
Insurance claims
Compliance-heavy systems
Complex business workflows
It is also useful when:
Multiple read models
Replay
Historical reconstruction
Auditability
What-if analysis
are important requirements.
When Event Sourcing Is a Bad Fit
Do not use Event Sourcing simply because:
"We are building microservices."
or:
"Everyone uses Kafka."
or:
"Event-driven architecture is modern."
For a simple CRUD application:
User
Product
Category
Address
traditional relational persistence may be much easier.
If the requirement is simply:
Store current state
Query current state
Update current state
Event Sourcing may provide more complexity than value.
Event Sourcing Is Not a Performance Silver Bullet
It is tempting to say:
Append-only = fast
therefore:
Event Sourcing = faster
That conclusion is too simplistic.
You still need to consider:
Event serialization
Event-store reads
Projection processing
Network communication
Replay cost
Storage
Snapshots
Concurrency
Consumer lag
Event Sourcing can improve certain workloads, particularly append-heavy workloads and systems where projections can be optimized independently, but it is not automatically faster for every application.
Event Sourcing Is Not Microservices
You can implement Event Sourcing inside:
Monolith
For example:
Spring Boot
|
+-- Order Aggregate
+-- Event Store
+-- Projection
No microservices are required.
Likewise, you can build microservices without Event Sourcing.
These are independent architectural decisions.
Event Sourcing Inside a Monolith
A perfectly reasonable architecture is:
Spring Boot
|
+------------+------------+
| | |
Orders Payments Shipping
| | |
+------------+------------+
|
Event Store
|
Projection Layer
|
Read DB
This can provide many Event Sourcing benefits without immediately introducing distributed systems complexity.
Event Sourcing and Microservices
In a distributed architecture:
Order Service
|
v
Order Event Store
|
v
Event Bus
|
+---+---+---+
| | | |
v v v v
Payment Inventory Shipping Analytics
Each service can maintain its own state based on events.
This can be powerful.
It can also introduce:
Eventual consistency
Duplicate delivery
Ordering problems
Network failures
Schema compatibility
Replay complexity
Distributed transactions
The architectural benefits must justify those costs.
The Complete Architecture
A production-style Event Sourcing + CQRS architecture might look like this:
Client
|
v
API Gateway
|
v
Command Handler
|
v
Domain Aggregate
|
+--------+--------+
| |
Load Events New Events
| |
v v
Event Store <------ Append
|
|
+----------------------+
| |
v v
Projection Event Bus
| |
v +-------+-------+
Read Database | | |
| v v v
v Search Notification
Query API Analytics
|
v
Client
The responsibilities are clear:
Command side
Enforces business rules
Event store
Stores authoritative history
Projection
Creates query-optimized state
Read database
Serves queries efficiently
Event bus
Distributes events
External consumers
React to domain changes
A Complete Order Example
Consider an order:
POST /orders
Command:
CreateOrder
The aggregate produces:
OrderCreated
Then:
AddItem
produces:
ItemAdded
Then:
PayOrder
produces:
PaymentCompleted
Then:
ShipOrder
produces:
OrderShipped
The event stream becomes:
v1 OrderCreated
v2 ItemAdded
v3 PaymentCompleted
v4 OrderShipped
A projection creates:
orders_read
with:
id = 42
status = SHIPPED
total = 250
customer = 1001
The database row is now just a convenient representation.
The actual history is:
OrderCreated
↓
ItemAdded
↓
PaymentCompleted
↓
OrderShipped
Failure Scenario: Projection Is Down
Suppose:
OrderShipped
is successfully appended.
But:
OrderProjection
is down.
The system now has:
Event Store
|
| OrderShipped
|
v
Projection
X
The read model may still say:
PAID
while the event store says:
SHIPPED
Once the projection recovers:
Replay / retry
|
v
OrderShipped
|
v
Read Model
|
v
SHIPPED
The system converges.
Failure Scenario: Consumer Crashes
Suppose:
OrderPaid
is published.
A notification service receives it and crashes.
The event remains available for retry or replay depending on the event-distribution architecture.
After recovery:
OrderPaid
|
v
Notification Service
|
v
Email Sent
The consumer should be designed to safely handle redelivery.
Failure Scenario: Read Database Is Lost
Suppose the read database is destroyed.
The event store remains:
v1 OrderCreated
v2 ItemAdded
v3 PaymentCompleted
v4 OrderShipped
...
Create a new read database:
Event Store
|
v
Replay
|
v
New Read Database
The system reconstructs the derived state.
This is one of the most compelling operational properties of Event Sourcing.
Failure Scenario: A New Projection Is Needed
The business asks:
Show the average time from order creation to payment.
No new source-of-truth data may be necessary.
Build:
PaymentTimingProjection
Replay:
OrderCreated
PaymentCompleted
Calculate:
paymentTime - creationTime
Store:
order_id
creation_time
payment_time
duration
The new feature can be built from historical events.
The Mental Model
The easiest way to understand Event Sourcing is to stop thinking:
Database stores objects.
and start thinking:
Database stores facts about what happened.
The current object is derived.
FACTS
|
+---------+---------+
| | |
v v v
State Reports Projections
The event history becomes the foundation.
CRUD vs Event Sourcing
Traditional CRUD:
Current State
|
+--------+--------+
| |
Read Update
| |
v v
Database Database
Event Sourcing:
Events
|
+-------+-------+
| |
Replay Project
| |
v v
Aggregate Read Model
CRUD asks:
What is the state?
Event Sourcing can answer:
What is the state?
but also:
How did we get here?
What happened before?
What was the state then?
Can we rebuild it?
Can we create another view?
That is the fundamental difference.
The Evolution of the Architecture
A system does not need to jump directly into full Event Sourcing.
A reasonable evolution can be:
1. Traditional CRUD
|
v
2. Separate Commands and Queries
|
v
3. CQRS
|
v
4. Optimized Read Models
|
v
5. Domain Events
|
v
6. Event Store
|
v
7. Event Sourcing
|
v
8. CQRS + Event Sourcing
|
v
9. Projections + Replay + Snapshots
This is important because Event Sourcing is not an all-or-nothing decision for an entire organization.
It can be introduced where its benefits are strongest.
Event Sourcing, CQRS, Saga and Outbox Together
These patterns solve different problems.
CQRS
|
+-- Separates command and query responsibilities
|
Event Sourcing
|
+-- Stores state changes as immutable events
|
Transactional Outbox
|
+-- Reliably publishes changes from transactional storage
|
Saga
|
+-- Coordinates long-running distributed business workflows
|
Idempotency
|
+-- Makes retries safe
A large distributed system may use all of them.
But using all of them everywhere is usually unnecessary.
Architecture should follow the problem.
The Most Important Design Questions
Before adopting Event Sourcing, ask:
1. Does historical state matter?
2. Do we need a complete audit trail?
3. Do we need to reconstruct state?
4. Do we need multiple independent projections?
5. Do we need replay?
6. Are business events meaningful?
7. Can the team operate an event-driven architecture?
8. Can we handle event schema evolution?
9. How will we handle privacy and deletion?
10. How will we handle projection failures?
11. How will we handle snapshots?
12. How will we monitor replay and lag?
13. How will we handle concurrent commands?
14. What is the expected event volume?
15. Is the complexity justified?
If most answers are:
No
traditional persistence is probably a better choice.
Common Mistakes
Mistake 1: Using Event Sourcing everywhere
Not every CRUD table needs an event stream.
Mistake 2: Treating Kafka as the event store
A message broker and an event store have different responsibilities.
Mistake 3: Storing state changes instead of business events
Prefer:
OrderCancelled
over:
status = CANCELLED
when the business meaning matters.
Mistake 4: Forgetting optimistic concurrency
Concurrent commands can produce invalid histories.
Mistake 5: Ignoring event versioning
Events can live for years.
Mistake 6: Replaying millions of events every time
Use snapshots where appropriate.
Mistake 7: Making projections non-idempotent
Consumers will eventually encounter retries or duplicates.
Mistake 8: Assuming eventual consistency doesn't matter
The UI must be designed with projection lag in mind.
Mistake 9: Treating projections as authoritative
They are derived.
The event store is authoritative.
Mistake 10: Ignoring storage growth
An append-only history continuously grows.
Putting Everything Together
A useful mental model is:
COMMAND
|
v
+----------------+
| Domain Model |
| / Aggregate |
+----------------+
|
Business Rules
|
v
EVENTS
|
v
+----------------+
| Event Store |
| Source of Truth|
+----------------+
|
+---------+---------+
| |
v v
Rehydration Projection
| |
v v
Aggregate Read Model
|
v
Queries
And for a distributed system:
Command
|
v
Domain Aggregate
|
v
Event Store
|
+--------------+--------------+
| | |
v v v
Projection Event Bus Analytics
| |
v +-----> Payment
Read Store |
| +-----> Shipping
v |
Query API +-----> Notification
The fundamental rule is:
Events are the history.
State is derived from the history.
Read models are optimized projections.
Final Takeaway
Event Sourcing changes the way an application thinks about data.
Traditional systems say:
Store the current state.
Event Sourcing says:
Store what happened.
Derive the current state from what happened.
Instead of:
Order
status = SHIPPED
we retain:
OrderCreated
ItemAdded
PaymentCompleted
OrderShipped
That history becomes the source of truth.
From it we can derive:
Current Order
Read Models
Reports
Search Indexes
Analytics
Audit History
Historical State
The real power of Event Sourcing is therefore not simply append-only storage.
It is the fact that the history of the domain becomes data that can be replayed, inspected, projected, and reconstructed.
But that power comes with substantial complexity.
Event Sourcing should be introduced when:
The history itself has business value
not simply because:
Events are popular
Microservices are popular
Kafka is popular
For systems where auditability, historical reconstruction, replay, multiple projections, and complex domain behavior are central requirements, Event Sourcing can fundamentally simplify problems that are difficult to solve with traditional CRUD.
For simple applications, however, ordinary state-based persistence is often the better engineering decision.
The goal is not to replace CRUD.
The goal is to recognize when the history of change is more valuable than the final state alone.