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
| Artifact | How derived | Latency |
|---|---|---|
| Entity snapshot | Core rules applied to events for that entity | Synchronous |
| Projection table row | Projection worker applies projection rules to event | Async (Kafka lag) |
| Query result | SQL against projection tables | Read-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
| Component | Technology | Role |
|---|---|---|
| Ledger | PostgreSQL (R2DBC) | Append-only event store |
| Snapshot store | PostgreSQL | Current entity state |
| Fan-out | Kafka | Async delivery to projection worker |
| Projection store | PostgreSQL | Materialized query views |
| Compiler | causet-compiler | DSL → causet.runtime.json + causet.projections.json |
| Runtime | causet-runtime-service | Intent processing, rule evaluation, event commit |
| Projection worker | causet-projection-worker | Event consumption, snapshot + projection updates |
| Query service | causet-query-service | Read 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.