Event sourcing stores state as a sequence of domain events. The current state is derived by replaying those events, not by reading one mutable row and treating it as the only truth.
An order service does not only keep status = shipped. It keeps OrderPlaced, PaymentCaptured, ShipmentCreated, and rebuilds the current order from that history.
State vs history
| Approach | Source of truth | Typical read path |
|---|---|---|
| CRUD state storage | Current row or document | SELECT the latest values |
| Event sourcing | Append-only event log | Replay events, or read a projection |
Event sourcing makes what happened explicit. That helps auditing, debugging, and rebuilding read models after schema changes.
flowchart TB Command["Command"] --> Aggregate["Aggregate"] Aggregate --> EventStore["Event store"] EventStore --> ProjectorA["Billing projection"] EventStore --> ProjectorB["Order status view"] EventStore --> ProjectorC["Search index"] EventStore -->|"integration events"| Broker["Message broker"] ProjectorB --> ReadAPI["Query API"]
Core concepts
- Event, an immutable fact in past tense, such as
OrderPlaced. - Event store, append-only storage for events, usually partitioned by stream.
- Stream, ordered events for one aggregate instance, such as
order-123. - Aggregate, consistency boundary that decides which new events are valid.
- Projection, a read model built by consuming events.
- Snapshot, optional cached state after N events to speed replay.
Ordering matters inside a stream. Global ordering across the whole system is usually unnecessary and expensive.
How a command becomes state
- Load the aggregate’s event stream, or start empty.
- Apply business rules to the command.
- Append zero or more new events if the command is valid.
- Publish integration events or update projections asynchronously.
INSERT INTO order_events (stream_id, version, type, payload, occurred_at)
VALUES (
'order-123',
4,
'OrderShipped',
'{"orderId":"order-123","carrier":"ups","trackingId":"1Z999"}',
now()
);The version or sequence number prevents conflicting writes to the same stream. Two writers racing on one aggregate should fail safely instead of corrupting history.
Projections and replay
Most systems do not replay the full log on every query. Projectors consume events and maintain query-friendly views.
Replay is still a core capability:
- Rebuild a read model after a bug in projector code.
- Add a new projection without rewriting historical commands.
- Recover analytics or audit views from the same source events.
Replay assumes events are immutable. Fixing mistakes means compensating with new events, not editing old ones.
Relationship to CQRS and EDA
Event sourcing and CQRS are often used together but are not the same thing.
- CQRS separates command and query paths.
- Event sourcing chooses events as the write-side source of truth.
A CRUD service can use CQRS without event sourcing. A service can use event sourcing with shared read and write models. In practice, event sourcing usually implies projections, which fits full CQRS well.
Downstream systems still benefit from event-driven architecture. Integration events can be published from projectors or from a relay after events are stored. If the write model is a normal database rather than an event store, the outbox pattern solves a different problem, keeping CRUD commits and broker publication aligned.
Snapshots and retention
Replaying thousands of events per aggregate gets slow. Snapshots store materialized state at a known version so replay starts from a checkpoint.
Retention is a product and compliance decision:
- How long must raw events be kept?
- Can sensitive fields be redacted in projections while events remain encrypted?
- When can archived streams leave hot storage?
Plan storage growth up front. Event sourcing trades update simplicity for append volume.
When to use it
Event sourcing helps when:
- Audit history and “why is it in this state?” matter legally or operationally.
- Multiple read models must be rebuilt from the same facts.
- Temporal queries matter, such as account balance at a past date.
- The domain naturally fits domain events inside domain-driven design.
When to skip it
Avoid event sourcing when:
- The team only needs current state and simple CRUD is enough.
- Strong immediate consistency is required everywhere with no projection lag.
- Operators cannot manage projector failures, replay jobs, and schema evolution.
- The domain has no stable event vocabulary yet.
A transactional table plus outbox pattern is often simpler for publishing integration events from conventional state storage.
Trade-offs
- Excellent auditability and rebuild options.
- More moving parts than CRUD plus projections as an afterthought.
- Read models are often eventually consistent with the write stream.
- Event schema evolution must be deliberate. Additive changes are safer than redefining old event meaning.
- Debugging improves for history, but projector bugs can produce wrong views until replayed.
Event sourcing is a persistence and modeling choice. Use it when the value of durable history and rebuildable projections outweighs the operational cost.
