$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[April 22, 2025] PostgreSQL

PostgreSQL Event Sourcing: Append-Only Patterns

PostgreSQL Event Sourcing: Append-Only Patterns

────────────────────────────────────────────────────────

PostgreSQL Event Sourcing: Append-Only Patterns

Tip: Event Table Design

CREATE TABLE events (
    id BIGSERIAL PRIMARY KEY,
    aggregate_id UUID NOT NULL,
    event_type TEXT NOT NULL,
    payload JSONB NOT NULL,
    created_at TIMESTAMPTZ DEFAULT now()
);

All state changes are recorded as events.

Gotcha: Never Update or Delete Events

Event sourcing requires an immutable event log. Use soft deletes if you must "remove" events.

Tip: Reconstruct State

SELECT payload FROM events
WHERE aggregate_id = 'uuid'
ORDER BY created_at;

Apply events in order to rebuild current state.

Gotcha: Performance for Long Aggregates

Reconstructing state from thousands of events is slow. Use snapshots to cache intermediate state.

Tip: Projections with Triggers

CREATE OR REPLACE FUNCTION project_order() RETURNS trigger AS $$
BEGIN
    -- Update read model
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Maintain a denormalized read model alongside events.

Gotcha: Event Versioning

Include a version field in events to handle schema evolution over time.

Tip: EXPLAIN (ANALYZE, BUFFERS) Is Your Best Friend

For query debugging, always use EXPLAIN (ANALYZE, BUFFERS) instead of plain EXPLAIN. The BUFFERS option shows hit/miss rates for every node, revealing whether your indexes are actually in memory.

Tip: Partial Indexes Are Underutilized

CREATE INDEX ON orders (status) WHERE status = 'pending' creates a tiny index that covers only the rows your query needs. It's faster to scan and cheaper to maintain than a full-column index.

Gotcha: NULL Sorting Is Non-Obvious

By default, NULLs sort AFTER non-null values in ascending order. ORDER BY col DESC puts NULLs FIRST. Use NULLS LAST or NULLS FIRST to be explicit.

Senior Insight

PostgreSQL is surprisingly well-suited for event sourcing. Using JSONB for event payload, an identity column for event ordering, and LISTEN/NOTIFY for real-time projections, you can implement event sourcing without additional infrastructure. I've built production event-sourced systems that handle millions of events on a single PostgreSQL instance. The key: partition the events table by month and create covering indexes for replay queries.

Source: pganalyze Blog (https://pganalyze.com/blog), PostgreSQL Docs (https://www.postgresql.org/docs/current/), Crunchy Data Blog (https://www.crunchydata.com/blog)

────────────────────────────────────────────────────────
<-- back to posts