This document is the source of truth for terminology and naming conventions in
the event-sorcery codebase. Code names must be consistent with this document.
The first half is a glossary of the CQRS/event-sourcing concepts the library is
built around; the second half codifies the conventions the library imposes on
top.
For an overview of the library itself, see SPEC.md. For usage patterns, see docs/cqrs.md.
An immutable record of something that happened in the domain. Past tense,
declarative (OrderPlaced, BalanceCredited). Once persisted, an event is the
source of truth — it cannot be edited, deleted, or rewritten. To "undo" an event
you emit a compensating event.
In code: a variant of an Event enum on an EventSourced type, paired with a
DomainEvent impl that supplies a stable event_type() string and an
event_version() for schema versioning.
The append-only log of all events, indexed by
(aggregate_type, aggregate_id, sequence). The library's event store is the
events table in SQLite, accessed through cqrs-es's
PersistedEventRepository trait (implemented by sqlite-es).
A consistency boundary: a cluster of state that's loaded, mutated, and persisted
atomically. All events for a single aggregate are totally ordered; events across
different aggregates are not. In cqrs-es vocabulary, trait Aggregate is the
bridge between domain logic and the event store.
In event-sorcery, consumers don't implement Aggregate directly. They
implement EventSourced on their domain type, and a blanket impl on
Lifecycle<Entity> provides Aggregate. The aggregate type's stable identifier
is EventSourced::AGGREGATE_TYPE.
The strongly-typed identifier for a single aggregate instance
(EventSourced::Id). Must be Display + FromStr so cqrs-es can stringify it
at the storage boundary. Use a newtype, not a raw String or Uuid, so the
compiler can prevent passing the wrong identifier.
The input that drives an aggregate's state transition (PlaceOrder, Credit).
Commands express intent, in imperative form. They produce events or fail; they
never mutate state directly.
event-sorcery splits command handling into two methods:
EventSourced::initialize(command, services)— for aggregates that don't yet exist. No&self, so handlers can't accidentally read state during creation.EventSourced::transition(&self, command, services)— for live aggregates. Receives the domain type, never the wrappingLifecycle.
The pure function that derives new aggregate state from old state plus an event. Split into:
EventSourced::originate(event)— create initial state from a genesis event. ReturnsSome(state)for events that bootstrap the aggregate,Nonefor events that require existing state.EventSourced::evolve(&self, event)— derive new state from an event applied to existing state.Ok(Some(new))on success,Ok(None)if the event doesn't apply,Errfor domain failures (overflow, invariant break).
A read model derived from events. The library's Projection<Entity, Backend> is
a SQLite-backed materialized view: a denormalized table that mirrors live
aggregate state for fast queries (load, load_all, filter).
A projection is fully derived — it can be dropped and rebuilt from the event log at any time without data loss.
cqrs-es calls the per-aggregate row in a projection's table a "view". The
library uses View only inside the view_backend GAT bound; consumers work
with Projection directly.
Pluggable storage for projections. trait ViewBackend is a higher-kinded type
emulation: it supplies, per (View, Aggregate) pair, a concrete
ViewRepository implementation. The default SqliteViewBackend maps every pair
to SqliteViewRepository. Tests use bespoke in-memory backends. See
crates/event-sorcery/src/view_backend.rs.
A periodic checkpoint of an aggregate's state, stored separately from the event
log so reload doesn't always replay every event. Snapshots are serialized with a
snapshot_version so a schema bump can invalidate them without touching the
event log.
Optional deletion of events at or before a snapshot's sequence number. Trades
replay latency for storage. Per-aggregate CompactionPolicy: Retain (default,
safe) or CompactAfterSnapshot. Compaction never crosses snapshot boundaries.
A side-effect handler keyed off events. Reads from one or more aggregates'
streams and produces effects (commands on other aggregates, external calls).
Retry with exponential backoff on optimistic-lock conflicts is
Projection::react's own internal behavior, not a property of reactors in
general -- a bespoke reactor opts into an equivalent retry for transient SQLite
busy errors via RetryOnBusy/IdempotentReactor or retry_with_backoff (see
docs/cqrs.md); one that does neither swallows a busy error
without retry, leaving its write outcome unknown.
Per-aggregate u64 (EventSourced::SCHEMA_VERSION) bumped whenever the shape
of state, events, or projections changes. Compared against the persisted version
in the schema_registry table on startup; mismatches clear stale snapshots and
trigger view rebuilds.
A durable, retryable unit of work an ENTITY kicks off for a worker (Job,
ADR-0009). It has an Origin entity and carries everything the round trip needs
(including the origin's id via origin_id()). The entity declares the job types
it may dispatch as EventSourced::Jobs (a JobList, built with the jobs!
macro, or Nil). There is no service-injection into handlers -- inputs a
handler needs are carried on the command; side effects are jobs.
A Job implements submit/reconcile, not perform: the framework routes the
FIRST execution to submit and every later one to reconcile, which must
determine the earlier attempt's fate and return a Reconciliation verdict
(Settled / NotSubmitted / Indeterminate). Every execution receives a
JobContext carrying the job's JobId (a ULID, stable across retries -- the
root for external idempotency keys) and the durable attempt count. Failures
state their retry class explicitly at the return site: JobFailure::Transient
retries with backoff, JobFailure::Terminal dead-letters immediately
(DeadReason::Rejected). There is deliberately no From impl for JobFailure,
so ? cannot silently classify a failure.
Origin-less background work (StandaloneJob, ADR-0007): reactors, pollers, job
chains, and startup recovery enqueue these directly on JobRuntime::enqueue --
there is no command commit to ride and no entity waiting on the verdict.
Implements perform. Every Job is also a StandaloneJob via the framework's
blanket impl (that is how the worker runs it); consumers only write
StandaloneJob impls for genuinely origin-less work.
Never pass a Job (an entity-kicked job) to JobRuntime::enqueue. The
blanket impl makes it type-check, but a standalone enqueue skips the
Dispatched event, so the origin's DispatchedJob field stays Idle and the
eventual verdict delivery is refused permanently (DispatchRefused, surfaced as
a VERDICT REFUSED operator alert). Entity-kicked jobs are dispatched only
through Effect::Dispatch.
What a command handler returns (Effect, ADR-0009): either domain events, or
exactly one JobDispatch -- the wiring-proven kick-off of a job, obtained from
the state guard DispatchedJob::dispatch(job) in transition, or from the
infallible Effect::kickoff(job) in initialize (no dispatch state exists
yet). This is the whole durability contract: the framework (never the handler)
emits the Dispatched event (which carries the job value -- the intent IS the
job) and enqueues in the same transaction.
Handler arms return through fx(value), which wraps any outcome -- a single
event, a collection of events, settle's Vec<DispatchEvent<J>>, a bare Job
(the initialize-side kick-off), a guarded JobDispatch, or the domain error --
into the full Result<Effect, Error>.
The entity embeds a DispatchedJob<J> field
(Idle -> InFlight -> Confirmed | Failed) and nests DispatchEvent<J> in its
event enum (conventionally a Dispatch variant, with the settling
DispatchOutcome<J> command conventionally named Settle); the machine owns
the state guard (refuse overlapping dispatches, absorb duplicate verdict
delivery), and DispatchedJob::originate(event) folds the first dispatch event
in the entity's originate. Settled verdicts (Settled / SettledFailure,
arriving as an opaque DispatchOutcome command) are SEALED -- only the
framework's delivery path constructs them, so Confirmed in entity state proves
the job settled. Delivery goes through an OriginPort (implemented by Store
when the origin's command enum absorbs DispatchOutcome<J> via From) BEFORE
the job acks; failed delivery defers rather than counting an attempt.
The pub(crate) enum that wraps EventSourced so it satisfies
cqrs-es::Aggregate. Has variants for Uninitialized, Live(Entity), and
Failed { error, last_valid_entity }. Implementation detail — Lifecycle is
not part of the library's public API and must not appear in any public bound.
Store<Entity> is the typed front door for sending commands.
StoreBuilder<Entity> wires a Store together with its projection and reactors
at startup, using a typed-list encoding (Cons/Nil) so forgetting a wiring
step is a compile error.
cqrs-es names (Aggregate, Query, View, DomainEvent, CqrsFramework) are
deliberately avoided in our public surface. Consumers work with EventSourced,
Store, Projection, Reactor. When in doubt, ask: "does the name hint that
the consumer is using cqrs-es directly?" If yes, rename.
EventSourced::AGGREGATE_TYPE is a stable string written into the events
table. Once set, never change it — the same string must forever resolve to
the same aggregate. Convention: PascalCase matching the Rust type name
("Position", "OffchainOrder").
event_type() returns a stable string identifying the variant. Convention:
"{EventEnumName}::{Variant}". Bump event_version() when the variant's
payload shape changes; never edit a shipped variant.
For entities with type Materialized = Table,
Entity::PROJECTION = Table("...") declares the SQLite table name. Convention:
snake_case matching the entity, suffixed _view ("position_view",
"offchain_order_view").
Error variants store domain types, not opaque strings. Prefer
InvalidSymbol(Symbol) over InvalidSymbol(String). The compiler then prevents
the caller from accidentally formatting the symbol away too early.
When an aggregate needs to perform side-effects atomically with persistence, the cqrs-es Service pattern applies. Naming convention used across consumer code:
{Action}er— the trait describing the capability (OrderPlacer).{Domain}Service— the implementation (OrderPlacementService).{Domain}Manager— the orchestration layer that drives commands through the framework (OrderManager).
This convention is library-imposed, not framework-enforced, but the cqrs-es
Service parameter on aggregate handle is what makes it work.
When renaming a type, all related names must change: variable names, function names, parameters, test helpers. Zero mentions of the old name may remain. A type rename without updating the surrounding vocabulary is incomplete and confusing.