Event Sourcing (ES) is a persistence pattern that treats the history of an entity as the primary source of truth. Unlike traditional CRUD (Create, Read, Update, Delete) where the database stores a snapshot of the current state, ES stores an immutable, append-only log of every state change (events).
Current state Sat timetis the deterministic result of folding an initial stateS_0over a chronologically ordered sequence of eventsE:
In implementation terms, this means the state is a projection of history. This approach provides a perfect audit trail, the ability to time-travel (reconstruct state at any point in history), and simplified concurrency through append-only semantics.
As the event stream grows, replayingNevents becomes a performance bottleneck (O(N) reconstruction). Snapshotting introduces a performance shortcut.
sequence_number of the last event processed.Reconstruction Logic with Snapshots:
public OrderAggregate load(String aggregateId) {
Snapshot<OrderState> snap = snapshotStore.load(aggregateId);
OrderAggregate aggregate = new OrderAggregate(snap.getState());
// Resume replay from the event immediately following the snapshot
List<Event> events = eventStore.readStream(aggregateId, snap.getVersion() + 1);
for (Event e : events) {
aggregate.apply(e);
}
return aggregate;
}
Projections (Materialized Views) transform the raw event stream into a format optimized for specific query patterns. This is the "Query" side of CQRS.
Most event brokers (Kafka, RabbitMQ) guarantee at-least-once delivery. Projection handlers must be idempotent to prevent data corruption during retries.
Implementation Patterns for Idempotent Projections:
event_id or sequence_number as a unique constraint in the read-model table.last_processed_sequence alongside the read model in the same transaction.-- PostgreSQL Atomic Projection Update
BEGIN;
UPDATE user_account_summary
SET balance = balance + :amount, last_event_id = :event_id
WHERE user_id = :user_id AND last_event_id < :event_id;
COMMIT;
When projection logic changes (e.g., adding a new field or fixing a calculation bug), the read model is "replayed":
Events are immutable, but code is not. If an event schema changes (e.g., OrderPlaced gains a currency field), you cannot rewrite the history.
Upcasting Strategies:
apply method (often leads to "pollution" of the domain model).Upcaster Example:
public class OrderPlacedUpcaster implements Upcaster {
public JsonNode upcast(JsonNode eventJson) {
if (!eventJson.has("currency")) {
((ObjectNode) eventJson).put("currency", "USD"); // Default for legacy events
}
return eventJson;
}
}
Crucial Rule: The apply(Event e) method in an Aggregate must be a pure function.
apply.Instant.now() or random numbers inside apply.Failure to follow this rule makes state reconstruction non-deterministic, rendering the event log useless for recovery.