Skip to content

State

Envelope, invariants, read models.

Every state change emits an immutable event; aggregate state is rebuilt by folding the stream. The store is the system of record; current state is a derived view. For the storage pick and wire-format specifics, see Stack/Data.

Envelope

Every event has two halves. The envelope carries identity, ordering, and audit:

Field Role
event id event (dedup key for subscribers)
stream type aggregate kind (subscriber routing)
stream id aggregate instance
event type fact class (routing, schema evolution)
per-stream version order within stream (optimistic concurrency on append)
global ordering position order across store (projection cursor)
principal id emitter (audit, future ReBAC hook)
occurred at wall-clock time of emission

The payload carries the change. Primitives only; the evolver re-validates by reconstructing value objects on read (see Reference/Modeling).

Subscribers route on (stream type, event type) and dedupe on event id.

Invariants

  • Append-only at the storage layer. Immutability is enforced where state lives, not in application code; the application identity cannot mutate or remove existing events.
  • Gap-free ordering for cursors. Projection workers advance against committed transactions only; in-flight writes never appear ahead of older ones.
  • Optimistic concurrency by version. Writers assert the expected per-stream version on append; a mismatch fails the transaction. No advisory locks, no last-write-wins.

Read models

Reads are not the inverse of writes. The write path goes through a decider and emits events; the read path projects off them, with no decider and no new events.

  • Fold-on-read. Single-aggregate read replays the aggregate's stream on every request. Cost grows with stream length; no snapshots yet.
  • Projection workers. List, filter, search. Background processes tail the store, maintain a per-projection denormalised table, and advance a bookmark. Per-projection logic plugs into a generic registry.

Where lifecycle timestamps live

Wall-clock timestamps (created_at, versioned_at, deprecated_at) belong on projections, not aggregate state. The envelope's occurred_at is the source of truth; projections derive timestamp columns from it as events arrive. State stays narrow and concerned with invariants; the read side answers "when did X happen?" by joining the projection row.

Shipped across Method, Plan, Practice, Capability, Family, and Agent (Path C). The Surface aggregate goes one step further and carries no lifecycle timestamps at all — they're rarely useful for an immutable config-time registry.