Skip to main content
reactive microservices architecture transactional outbox and event sourcing with java 21

Event Sourcing and Aggregate Design

3 min read Chapter 5 of 10
Summary

Event Sourcing persistence pattern stores state as immutable...

Event Sourcing persistence pattern stores state as immutable events, using PostgreSQL and Kafka for implementation.

Event Sourcing and Aggregate Design

Introduction to Event Sourcing

Event Sourcing is a persistence pattern where state is not stored directly; instead, all changes to application state are stored as a sequence of immutable events. This approach allows for the reconstruction of an application’s state at any point in time by replaying the events. The Aggregate Root, the entry point and consistency boundary for a cluster of associated objects, is responsible for maintaining invariants and processing commands.

Implementing Event Sourcing with PostgreSQL

PostgreSQL can be used to implement an Event Store using an append-only table with a BIGSERIAL or IDENTITY column for global ordering. Optimistic Concurrency in PostgreSQL Event Stores is achieved using a version column on the Aggregate table and ‘check-then-insert’ logic within a transaction. The following SQL code snippet illustrates a minimal PostgreSQL schema for Event Sourcing and Snapshotting:

CREATE TABLE event_store (
    global_offset BIGSERIAL PRIMARY KEY,
    stream_id UUID NOT NULL,
    stream_version INT NOT NULL,
    event_type TEXT NOT NULL,
    payload JSONB NOT NULL,
    occurred_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    UNIQUE (stream_id, stream_version)
);

CREATE TABLE aggregate_snapshots (
    stream_id UUID PRIMARY KEY,
    last_version INT NOT NULL,
    state_data JSONB NOT NULL,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

Aggregate Snapshotting

Aggregate Snapshotting is an optimization technique where the current state of an aggregate is periodically persisted to avoid replaying the entire event history from the beginning of time. This technique reduces the computational complexity of state reconstruction from O(N) to O(1) where N is the number of events. Snapshots are typically stored in a separate table containing the serialized aggregate state and the last processed event version (last_event_id).

Using Kafka for Event Ordering

Kafka requires a custom ‘Partition Key’ (typically the Aggregate ID) to guarantee strict ordering of events within a specific partition. However, Kafka partitions provide total ordering only within the partition; global ordering across partitions is not guaranteed. The Transactional Outbox pattern can be used to prevent the ‘Dual Write Problem’ by ensuring event publication is atomic with aggregate state changes.

Java 21 Records for Immutable Event Payloads

Java 21 Records facilitate immutable event payloads, aligning with the functional transformation requirements of Event Sourcing. The following Java code snippet illustrates state transformation using Java 21 Pattern Matching and Records:

public record AccountAggregate(UUID id, Long balance, int version) {
    public AccountAggregate apply(AccountEvent event) {
        return switch (event) {
            case MoneyDeposited d -> new AccountAggregate(id, balance + d.amount(), version + 1);
            case MoneyWithdrawn w -> new AccountAggregate(id, balance - w.amount(), version + 1);
        };
    }
}

## Architecture Component Mapping
The following table illustrates the architecture component mapping for Event Sourcing:
| Component | Tech Stack | Role |
|---|---|---|
| Persistence | PostgreSQL | Durable Event Store |
| Ordering | Kafka Partitions | Per-Aggregate Sequence Guarantee |
| Efficiency | Snapshots | Performance Optimization for Large Streams |
| CDC | Debezium | Streaming events from PG to Kafka |

## Sources
[1] PostgreSQL Documentation: Append-only tables
[2] Kafka Documentation: Partitioning