Skip to main content

Events and the append-only log

An event is an immutable, typed fact. Once appended it is never changed and never moved. The log only grows.

Anatomy of an event

Each event carries:

  • client_seq: a number the writer assigns, used for idempotent retries.
  • event_seq: assigned by the server. Counts events within the aggregate, starting at 1, continuing across batches.
  • event_id: optional 128-bit id of your choosing. Stored, never interpreted. Useful as your own idempotency key.
  • event_timestamp: the client's timestamp for when it happened in your domain. Stored as sent.
  • event_type_major / event_type_minor: the event type. Major marks a breaking change to the shape, minor a compatible one. Major must be non-zero (error 2001). Types tie into schema validation.
  • event_value: opaque bytes. You pick the encoding (JSON, Protobuf, whatever your services speak). Validated against a registered schema if one exists for the type.
  • iv: set when the payload is encrypted. You encrypt before writing; the server holds no keys and skips schema validation for these events.

Batches and the version

One write to one aggregate lands as one batch: the events, plus batch metadata the server stamps. The batch records the client_id of the writer, an optional user_id, and a server timestamp taken when the shard prepares the batch.

Each batch gets the next aggregate version, starting at 1. An aggregate's latest version is what optimistic concurrency checks against, and version 0 means the aggregate does not exist yet. So there are two counters: aggregate version counts batches, event_seq counts events. A batch of three events moves the version by 1 and event_seq by 3. Reads and trims work in aggregate versions.

A write that spans several aggregates produces one batch per aggregate, each with its own version.

Neither timestamp decides order. Order within an aggregate is the aggregate version and event_seq, assigned by the server. See Reads and ordering.

The aggregate version is not the event type's (major, minor). One is the stream's position, the other is the schema's revision.

Append-only, including retention

There is no UPDATE. Two retention operations exist: delete removes a whole stream, trim drops the batches before a given version. Both are recorded as tombstones appended to the log; compaction reclaims the bytes later. Neither alters the events that remain. See Retention and deletion.

Immutability is what makes the audit chain worth anything. If entries could be edited in place, hash-chaining them would prove nothing. The chain does not stop a stream being deleted wholesale; retention policy and backups govern that.