Home/Learn/Microservices/Event Sourcing

Event Sourcing

Advanced
Data Management

State is derived by replaying a log of immutable domain events rather than storing the current snapshot; enables audit trails, temporal queries, and event-driven projections.

Overview

Event Sourcing is a persistence pattern where the state of an aggregate is stored as an **ordered sequence of immutable domain events** rather than as the current snapshot. To reconstruct the current state, you replay all events from the beginning (or from the last snapshot). Examples: `OrderCreated → ItemAdded → PaymentReceived → OrderShipped`. Benefits: complete audit trail with no information loss, ability to query "what was the state at time T?", natural fit for CQRS and event-driven microservices, and the event log becomes the source of truth that downstream projections consume. Challenges: event schema evolution, potential replay latency for long histories (mitigated by snapshots), and the paradigm shift from CRUD thinking.

Event Store and Aggregate Replay

The **event store** is an append-only table. Aggregates are reconstructed by loading all their events and folding them into the current state via `apply()` methods. A **snapshot** is a periodic checkpoint of the aggregate state — replay starts from the latest snapshot rather than the very beginning, bounding the replay cost.

Java — event-sourced aggregate with replay
// Event base type
public sealed interface OrderEvent permits
    OrderCreated, ItemAdded, PaymentReceived, OrderShipped {}

public record OrderCreated(String orderId, String customerId, Instant at) implements OrderEvent {}
public record ItemAdded(String orderId, String sku, int qty, BigDecimal price) implements OrderEvent {}

// Aggregate — rebuilt by replaying events
public class Order {
    private String id;
    private String status;
    private List<OrderItem> items = new ArrayList<>();

    public static Order rebuild(List<OrderEvent> events) {
        Order order = new Order();
        events.forEach(order::apply);
        return order;
    }

    private void apply(OrderEvent event) {
        switch (event) {
            case OrderCreated e -> { this.id = e.orderId(); this.status = "CREATED"; }
            case ItemAdded e    -> items.add(new OrderItem(e.sku(), e.qty(), e.price()));
            case PaymentReceived e -> this.status = "PAID";
            case OrderShipped e -> this.status = "SHIPPED";
        }
    }
}

// Event store (append-only)
CREATE TABLE event_store (
    stream_id   VARCHAR(36),
    seq         BIGINT,
    event_type  VARCHAR(100),
    payload     JSON,
    occurred_at TIMESTAMP,
    PRIMARY KEY (stream_id, seq)
);

CQRS Read Models (Projections)

Because the event store is append-only and optimised for writes, read models (projections) are built separately by consuming the event stream. A projection subscribes to the event log (via Kafka, a DB changelog, or an event bus) and materialises a denormalised view optimised for queries. Multiple independent projections can exist: one for order summaries, one for analytics, one for the audit log.

Java — CQRS projection consuming event stream
// Projection — builds a read model from events
@Component
class OrderSummaryProjection {

    // Consumes events from Kafka (published by event store)
    @KafkaListener(topics = "order-events")
    public void on(OrderEvent event) {
        switch (event) {
            case OrderCreated e -> {
                orderSummaryRepo.save(new OrderSummary(
                    e.orderId(), e.customerId(), "CREATED", BigDecimal.ZERO));
            }
            case ItemAdded e -> {
                orderSummaryRepo.findById(e.orderId())
                    .ifPresent(s -> {
                        s.setTotal(s.getTotal().add(e.price().multiply(BigDecimal.valueOf(e.qty()))));
                        orderSummaryRepo.save(s);
                    });
            }
            case OrderShipped e ->
                orderSummaryRepo.updateStatus(e.orderId(), "SHIPPED");
        }
    }
}

// Query — reads from the fast, denormalised projection table
@GetMapping("/orders/{id}/summary")
OrderSummary getSummary(@PathVariable String id) {
    return orderSummaryRepo.findById(id).orElseThrow();
}

Event Schema Evolution and Upcasters

Immutable events stored forever create schema evolution challenges: you cannot alter existing events. The solution is **upcasting**: when loading old events, convert them to the latest version before applying them to the aggregate. Upcasters are pure functions that transform an old event JSON payload to the new schema. This keeps old events intact and evolution logic centralised.

Java — event schema evolution with upcasters
// v1 event (stored in event store forever)
// {"orderId": "123", "customerId": "456"}

// v2 event adds "channel" field
public record OrderCreatedV2(String orderId, String customerId, String channel) {}

// Upcaster: v1 → v2
public class OrderCreatedUpcaster implements Upcaster<OrderCreatedV1, OrderCreatedV2> {
    @Override
    public OrderCreatedV2 upcast(OrderCreatedV1 old) {
        return new OrderCreatedV2(old.orderId(), old.customerId(), "UNKNOWN");
    }
}

// Event store pipeline: load → upcast chain → apply to aggregate
List<OrderEvent> events = eventStore.load(orderId);
List<OrderEvent> upcast = upcasterChain.upcast(events);
Order order = Order.rebuild(upcast);

// Key principle: never modify stored events — add upcasters instead
// Chain upcasters for multi-version jumps: v1→v2→v3

Key Points to Remember

  • 1Event Sourcing stores facts (events), not state — current state is derived by replaying events
  • 2Append-only event store: OrderCreated → ItemAdded → PaymentReceived → OrderShipped
  • 3Snapshots bound replay cost for aggregates with long event histories
  • 4Read models (projections) are built separately by consuming the event stream — CQRS
  • 5Multiple independent projections can consume the same event stream for different read needs
  • 6Schema evolution: never modify stored events — add upcasters to transform old events to new versions

Interview Questions

Sign in to ask Aria
1

What is the difference between traditional CRUD and event sourcing?

EasyThoughtWorks
2

What is a snapshot in event sourcing and when would you use one?

MediumAxon
3

How do you handle schema evolution of events in an event-sourced system?

HardConfluent
4

What is the relationship between event sourcing and CQRS?

MediumNetflix
5

What are the main drawbacks of event sourcing compared to traditional CRUD?

MediumAmazon

Ask Aria about Event Sourcing

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…