Event Sourcing Primer: Rebuilding State Without a Database

This article explains how event sourcing replaces the traditional current‑state table with an immutable event log, allowing systems to reconstruct any state, handle concurrency, support auditing, and power flexible read models through projections, while outlining the necessary architectural components such as aggregates, event stores, snapshots, outbox, idempotency, and the natural transition to CQRS.

Ray's Galactic Tech
Ray's Galactic Tech
Ray's Galactic Tech
Event Sourcing Primer: Rebuilding State Without a Database

Why Current State Tables Are Not Enough

When a system fails, developers instinctively query the current state table to answer questions like why an order is cancelled or why a refund succeeded. However, most business tables only tell you what the state is now , not how it got there . This lack of process visibility is the problem event sourcing aims to solve.

What Event Sourcing Is

In an event‑sourced system the database does not store the current state directly; it stores every domain event that caused state changes. Any point‑in‑time state can be recomputed by replaying the ordered events.

Events are immutable facts written in the past tense (e.g., OrderCreated, OrderPaid). Commands (e.g., PayOrder) express intent and may fail, while events represent the confirmed outcome.

Events vs. Commands

Command : "I want to do something" – may be rejected.

Event : "Something has happened" – immutable and always true.

State as a Projection

The event stream is the source of truth, like a ledger. A projection (or read model) is a derived view optimized for queries, similar to a report generated from the ledger.

Minimal Working Example

Assume an orders table with columns order_id, user_id, status, amount, updated_at. Using only the event stream, the current state can be rebuilt in memory:

public enum OrderStatus { INIT, CREATED, PAID, SHIPPED, COMPLETED, CANCELLED }

public final class OrderState {
    private String orderId;
    private String userId;
    private BigDecimal amount;
    private OrderStatus status = OrderStatus.INIT;
    private String paymentNo;
    private String warehouseNo;
    // apply(event) updates fields based on event type
}

With the event list [OrderCreated, OrderPaid, OrderShipped], the apply method yields the final state without any orders table.

Fundamental Architecture: Command → Aggregate → Event Store → Projector → Read Model

Command API → Aggregate (replay events, validate command, emit new events) → Event Store (append‑only) → Projector (consume events, build read models) → Read Model (queries)

Each layer has a clear responsibility: Command API: receives intent, never mutates the database directly. Aggregate: the domain‑logic core that validates commands and produces events. Event Store: immutable, version‑controlled persistence of events. Projector: subscribes to the event stream and updates query‑optimized tables. Read Model: tables such as order_summary, order_detail_view, etc., used for UI and reporting.

Event Store Design

The store must:

Append events per aggregate ID in order.

Guarantee immutability.

Detect concurrent writes via optimistic version checks.

Support reading history and reading from a specific version.

Typical SQL schema (PostgreSQL/MySQL) includes columns aggregate_id, aggregate_version, event_id, event_type, payload, metadata, occurred_at. The composite primary key (aggregate_id, aggregate_version) enforces version ordering.

Concurrency Control

When two requests read version 3 of order ORD-10001, the first may append OrderCancelled with version 4. The second’s attempt to append OrderPaid with expected version 4 will fail, exposing the conflict rather than silently overwriting data.

Command Handling Flow

Load historical events for the aggregate.

Rehydrate the aggregate state.

Execute business command.

Emit one or more new events.

Append events with expected version (optimistic lock).

Trigger projection updates and event distribution.

Projection Implementation

Projections must be idempotent because event consumption is at‑least‑once. Typical strategies:

Record processed event_id.

Use last_event_version for conditional updates.

UPSERT statements.

Example projector for order_summary:

@Component
@RequiredArgsConstructor
public class OrderSummaryProjector {
    private final JdbcTemplate jdbcTemplate;
    public void project(StoredEvent storedEvent) {
        OrderEvent event = storedEvent.event();
        if (event instanceof OrderCreated e) {
            jdbcTemplate.update("""
                INSERT INTO order_summary (order_id, user_id, status, amount, payment_no, last_event_version, updated_at)
                VALUES (?, ?, ?, ?, ?, ?, ?)
                ON CONFLICT (order_id) DO NOTHING
                """,
                e.orderId(), e.userId(), "CREATED", e.amount(), null,
                storedEvent.aggregateVersion(), Timestamp.from(e.occurredAt()));
        } else if (event instanceof OrderPaid e) {
            jdbcTemplate.update("""
                UPDATE order_summary SET status = ?, payment_no = ?, last_event_version = ?, updated_at = ?
                WHERE order_id = ? AND last_event_version < ?
                """,
                "PAID", e.paymentNo(), storedEvent.aggregateVersion(),
                Timestamp.from(e.occurredAt()), e.orderId(), storedEvent.aggregateVersion());
        }
        // similar branches for OrderCancelled, OrderShipped …
    }
}

Production‑Ready Enhancements

Snapshots

When an aggregate’s event stream grows large, replaying from the beginning becomes costly. A snapshot stores the aggregate’s state at a given version, allowing recovery by loading the snapshot and then only the newer events.

CREATE TABLE order_snapshots (
    aggregate_id VARCHAR(64) PRIMARY KEY,
    aggregate_version BIGINT NOT NULL,
    payload JSONB NOT NULL,
    created_at TIMESTAMPTZ NOT NULL
);

Typical snapshot strategy: create a snapshot every 50–100 events or when reconstruction time exceeds a threshold.

Transactional Outbox

To avoid the classic double‑write problem (event stored but message not sent, or vice‑versa), write both the domain event and an outbox record in the same DB transaction. A background relay reads NEW outbox rows, publishes to Kafka (or another broker), and marks them SENT.

CREATE TABLE outbox_events (
    id BIGSERIAL PRIMARY KEY,
    aggregate_id VARCHAR(64) NOT NULL,
    event_id VARCHAR(64) NOT NULL UNIQUE,
    topic VARCHAR(128) NOT NULL,
    payload JSONB NOT NULL,
    status VARCHAR(16) NOT NULL,
    retry_count INT NOT NULL DEFAULT 0,
    next_retry_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL
);

Idempotency

Three layers need idempotency:

Command level – use a commandId or business key to reject duplicate intents.

Event store – unique event_id index prevents duplicate persistence.

Projection – use last_event_version or processed‑event tables to make updates safe against re‑processing.

Scaling via Partitioning

Concurrency is isolated per aggregate. Different aggregates can be processed in parallel, so horizontal scaling is achieved by sharding on aggregate_id (e.g., hash‑based DB partitions, Kafka partitions, or separate event‑store shards).

Replaying & System Rebuild

Because the event store is the source of truth, rebuilding a corrupted read model is simple: truncate the projection table and replay all events in order. This also enables adding new query models without touching the core domain logic.

When to Use Event Sourcing

Business processes where the evolution path matters more than the final state.

High audit, compliance, or traceability requirements.

Complex state machines with frequent concurrent updates.

Need for multiple, evolving read models (reports, dashboards, search indexes).

Scenarios requiring time‑travel, replay, or rebuilding after failures.

Typical domains: payments, order management, inventory reservation, workflow/approval, risk decision chains, loyalty points.

When Not to Use It

Simple CRUD‑only tables or configuration data.

Static reference data with no business process.

Small systems where history is irrelevant.

Adopting event sourcing adds modeling complexity, requires team mindset shift, and demands supporting infrastructure (snapshots, outbox, observability).

Natural Move Toward CQRS

Event sourcing separates the write side (commands, aggregates) from the read side (projections). This separation aligns perfectly with CQRS, which advocates distinct models for command handling and query handling. Event sourcing provides the immutable fact store; CQRS provides the pattern for using that fact store to feed independent read models.

Adoption Guide for First‑Time Teams

Select a well‑bounded, high‑value aggregate (e.g., order lifecycle) as a pilot.

Model the aggregate, implement event persistence, and build a basic projection.

Validate replay and state reconstruction.

Iteratively add production features: snapshots, outbox, additional projections, monitoring.

Observability must include event‑id/aggregate‑id tracing, projection lag metrics, outbox queue depth, replay job health, and dead‑letter alerts.

Key Takeaways

Traditional systems treat the current row as the source of truth; event‑sourced systems treat the event log as the source of truth.

State is merely a projection of immutable events.

This view enables auditability, replay, flexible read models, and natural concurrency control.

It also paves the way for CQRS, where write and read concerns are fully decoupled.

Adoption should be incremental, with strong observability and a clear understanding of added complexity.

In an event‑sourced system, the database row is a copy; the real original is the sequence of immutable business events.
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backend architectureidempotencysnapshotcqrsevent sourcingprojectionoutbox
Ray's Galactic Tech
Written by

Ray's Galactic Tech

Practice together, never alone. We cover programming languages, development tools, learning methods, and pitfall notes. We simplify complex topics, guiding you from beginner to advanced. Weekly practical content—let's grow together!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.