StateEvent-Sourced State

Event-Sourced State

Causet is built on event sourcing. The ledger_events table is the source of truth. Everything else — entity snapshots, projection tables, query results — is derived from it.

The Event Sourcing Pattern

In a conventional system, the database holds current state. State changes overwrite previous state. The history of how you got to the current state is often lost or reconstructed imperfectly from logs.

In an event-sourced system, the log of events is the database. Current state is computed by replaying events. Nothing is ever overwritten.

The Ledger

The ledger_events table is append-only. Events are never updated or deleted.

CREATE TABLE ledger_events (
    event_id        UUID PRIMARY KEY,
    event_type      TEXT NOT NULL,
    entity_type     TEXT NOT NULL,
    entity_id       TEXT NOT NULL,
    ts              BIGINT NOT NULL,
    sequence_num    BIGINT NOT NULL,
    payload         JSONB,
    intent_id       UUID,
    correlation_id  UUID
);

Every intent that passes preflight produces exactly one event. That event is committed to the ledger and published to Kafka for downstream processing.

Everything Else Is Derived

ArtifactHow derivedLatency
Entity snapshotCore rules applied to events for that entitySynchronous
Projection table rowProjection worker applies projection rules to eventAsync (Kafka lag)
Query resultSQL against projection tablesRead-time

None of these artifacts are authoritative. They can all be dropped and rebuilt from the ledger.

Audibility

Because every state change corresponds to a committed event, every state transition is auditable:

SELECT event_id, event_type, ts, payload
FROM ledger_events
WHERE entity_type = 'user'
  AND entity_id = 'user_abc123'
ORDER BY ts ASC;

This query returns the full history of every event that ever affected user_abc123, in order. You can trace every increment of concert_count, every entry in concerts_attended, and every saga transition.

No state change is unaccountable. If a field has an unexpected value, inspect the ledger to find the event that set it.

Replay

The ability to replay the ledger is one of event sourcing’s most powerful properties. Uses:

  • Backfilling new fields: add a new memory field to the state definition, replay the ledger, and the new field is populated for all entities from the beginning of history
  • Fixing bugs: if a core rule had a bug that incorrectly computed a field, fix the rule, replay the ledger, and the field is corrected for all affected entities
  • Debugging: replay a specific entity’s event stream to trace how its state evolved step by step
  • Auditing: reconstruct any entity’s state at any point in history

Temporal Queries

To reconstruct an entity’s state at a specific point in time, replay events up to that timestamp:

SELECT *
FROM ledger_events
WHERE entity_type = 'user'
  AND entity_id = 'user_abc123'
  AND ts <= 1719000000000
ORDER BY ts ASC;

Applying these events through the rules engine gives you the entity’s state as of ts = 1719000000000. This is useful for:

  • “What was this user’s concert count on a specific date?”
  • “What did the entity state look like when this intent was submitted?”
  • “Was this user eligible for this promotion at time T?”

The Trade-off

Event sourcing adds complexity to the write path:

  • Every state change requires an event definition
  • Intent → event → snapshot → projection is more layers than a direct database write
  • Eventual consistency between entity snapshots and projection tables requires care in the application layer

The payoff is a richer history, more robust auditing, and the ability to rebuild and extend the derived views without touching historical data.

Causet’s Implementation

ComponentTechnologyRole
LedgerPostgreSQL (R2DBC)Append-only event store
Snapshot storePostgreSQLCurrent entity state
Fan-outKafkaAsync delivery to projection worker
Projection storePostgreSQLMaterialized query views
Compilercauset-compilerDSL → causet.runtime.json + causet.projections.json
Runtimecauset-runtime-serviceIntent processing, rule evaluation, event commit
Projection workercauset-projection-workerEvent consumption, snapshot + projection updates
Query servicecauset-query-serviceRead path — SQL templates against projection tables

The ledger is stored in the same PostgreSQL instance as snapshots and projections, partitioned by tenant fork schema ({platformId}_{applicationId}_{forkId}). Multi-tenant isolation is at the schema level — different tenants never share tables.