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:

ConceptMeaning
CommandA request to change something; it may be rejected
Domain eventAn immutable, past-tense fact accepted by the domain
Decision stateTemporary state folded from relevant events to decide one command
ProjectionA 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:

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:

  1. reads the supplied DecisionContext;
  2. decodes matching records;
  3. folds their events from the model’s initial state through evolve;
  4. runs the model’s try function;
  5. encodes the proposed events;
  6. appends the batch only if the context remains stable;
  7. runs supported transactional subscriptions;
  8. commits and returns Recorded events.

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.

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:

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:

  1. decide using deterministic inputs;
  2. append the domain event;
  3. atomically persist durable work intent when the backend supports it;
  4. commit;
  5. 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:

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

Costs

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.

Search Document