September 7, 2026 78 minutes minutes read Admin

Event Sourcing - Storing State as a History of Events

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 PENDING to PAID within 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.