Event Sourcing and Command Dispatch
Event Sourcing stores the sequence of accepted domain events as the authoritative state of a system. Current state and query views are derived by folding or projecting those facts.
Instead of overwriting a course row from capacity = 20 to capacity = 25, an
event-sourced model can retain:
CourseDefined(course_id: c1, capacity: 20)
CourseCapacityChanged(course_id: c1, new_capacity: 25)
The history explains not only what is true now, but which accepted business facts made it true.
Factos stores accepted events, not commands, rejected attempts, or mutable domain objects.
Commands, events, state, and projections
These concepts have different jobs:
| Concept | Meaning |
|---|---|
| Command | A request to change something; it may be rejected |
| Domain event | An immutable, past-tense fact accepted by the domain |
| Decision state | Temporary state folded from relevant events to decide one command |
| Projection | A read-oriented view derived from committed events |
For example:
SubscribeStudentToCourse(s1, c1) command
StudentSubscribedToCourse(s1, c1) accepted event
CourseFullyBooked(c1) domain error, not an event
CourseCard(name, seats_remaining) projection row
Do not record every command as though it succeeded. Do not use a projection as the authoritative source for a rule unless the backend guarantees that view is updated in the same transaction and the design intentionally relies on it.
The event log
A Factos backend record contains:
- an application-supplied event id;
- a globally ordered sequence position;
- a stable event type and schema version;
- tags used for decision-context selection;
- application metadata;
- the encoded JSON payload.
Events are append-only facts. Correcting history normally means appending a new business fact, not editing the old record. Schema evolution is handled by version-aware application decoders.
Factos has no core stream or per-stream revision model. Backends expose an ordered log and select decision facts by event type and tags.
Dispatch is read, decide, then conditionally append
command + relevant accepted facts -> new events or domain error
A backend dispatch:
- reads the supplied
DecisionContext; - decodes matching records;
- folds their events from the model’s
initialstate throughevolve; - runs the model’s
tryfunction; - encodes the proposed events;
- appends the batch only if the context remains stable;
- runs supported transactional subscriptions;
- commits and returns
Recordedevents.
The application passes the context with every command:
configuration
|> factos_pog.dispatch(
command,
decision_context: decision_context(command),
event_id: new_event_id,
)
The same domain model and context can be dispatched through another Factos backend, while connection, transaction, retry, and subscription semantics remain backend-specific.
Consistency comes from the observed context
Reading relevant history produces both folded state and the highest observed global position. Factos expresses the optimistic append check as:
factos.FailIfEventsMatch(
decision_context: context,
after: observed_position,
)
The backend must not accept events from that stale decision if a new matching event appeared after the observed position.
NoContextreads nothing and appends unconditionally;Matching(items:)protects a selective type-and-tag predicate;AllEventsprotects the complete log.
This is the bridge between Event Sourcing and Dynamic Consistency Boundaries. The read predicate that supplies the decision also defines what concurrent change invalidates it. See Dynamic Consistency Boundaries for the full model.
Events are long-lived contracts
An event may outlive the code version that wrote it. Design for that reality:
- use past-tense names that describe accepted domain facts;
- keep store-visible type names stable;
- assign explicit schema versions;
- decode every supported historical version;
- preserve tags required by current and future decision contexts;
- keep operational metadata separate from facts required to rebuild decisions;
- test codecs against malformed and unsupported stored data.
If time, an id, or another value changes a future business decision, put the necessary value in the event payload. Metadata is appropriate for correlation, causation, actor, or audit information that does not participate in domain folding.
Projections turn facts into useful views
Replaying the full decision history for every screen or report is usually the wrong read path. A projection transforms committed events into a query-oriented shape:
StudentSubscribedToCourse
| |
v v
course occupancy student timetable
Projection tables are disposable only if the application can rebuild them from available history and has implemented the required recovery process.
Factos provides application-owned Subscription hooks for work that a backend
can run in the append transaction. PostgreSQL and SQLite can update projection
tables interactively before commit. Cloudflare D1 uses immutable transaction
plans with different semantics.
A subscription failure rejects and rolls back the originating append where the backend documents that guarantee.
External effects happen after commit
Do not call external services, publish to a non-transactional broker, send email, or read unstable inputs inside retrying decision code or a transaction callback. A backend may rerun that code after a transaction conflict.
The safe pattern is:
- decide using deterministic inputs;
- append the domain event;
- atomically persist durable work intent when the backend supports it;
- commit;
- execute external IO with an application-owned retry and idempotency policy.
Factos does not prescribe the worker or message-delivery architecture.
Simulation and integration tests prove different things
factos/simulate can:
- start from a list of domain events;
- encode and decode them through the real codec;
- dispatch commands through the real model;
- assert resulting events and domain or codec errors.
This is fast evidence for domain behavior and event compatibility. It cannot prove database transaction isolation because no database race occurs.
Backend projection simulators run real subscription logic against PostgreSQL or SQLite while event history remains in memory. They prove projection transaction behavior for the exercised path, not event-store concurrency.
Use dispatch integration tests against the deployed backend for append atomicity, retry behavior, and concurrent invariant protection.
Benefits
- a durable explanation of accepted business change;
- deterministic reconstruction of decision state;
- multiple projections from the same facts;
- natural audit and temporal analysis when the events contain the needed data;
- domain behavior testable as pure functions.
Costs
- event schemas and selection tags require compatibility discipline;
- projections need recovery, checkpointing, and monitoring;
- history grows and needs operational management;
- event-store correctness under concurrency is critical;
- deleting or redacting sensitive historical data is harder;
- reconstructing state can require snapshots or other application-owned optimisations at scale.
Event Sourcing is a modelling and operational commitment, not a default persistence pattern. Use it where accepted history and history-based decisions justify those costs.
Next
Read Dynamic Consistency Boundaries for the type-and-tag predicate and concurrency model, then use The Factos core model as the API reference.