Start Here: Why Factos Exists

Factos is a Gleam library for building event-sourced systems around the facts a business decision actually needs.

The short version is:

command + relevant accepted facts -> new facts or a domain error

Your application defines the commands, events, rules, errors, event codecs, and read models. Factos supplies the small shared model that connects those pieces to an event store, plus backends that make the read-decide-append flow safe under concurrent use.

This page explains the problem Factos solves, the value of its approach, and where each part of the library fits. No prior knowledge of Domain-Driven Design (DDD), Event Sourcing, or Dynamic Consistency Boundaries (DCB) is assumed.

The problem: a rule rarely belongs to one row

Consider a course-subscription system with these rules:

To decide whether student s1 may join course c1, the application needs facts about both that course and that student. Two requests may also race for the last seat. Reading two current database rows and updating them later is not enough: the answer may become stale between the read and the write.

A traditional event-sourced design often puts each entity in its own event stream. That makes a rule local to one stream easy to protect with an expected stream revision, but a rule spanning a student and a course now spans two streams. The usual remedies—larger aggregates, process managers, reservations, or compensation—can add coordination that the business rule did not ask for.

Factos takes a different approach: the consistency boundary is the set of facts used by this command. It does not have to be a permanent stream or object boundary.

The Factos approach

For SubscribeStudentToCourse(s1, c1), the application declares a decision context containing:

  1. course-definition, capacity-change, and subscription events tagged course:c1; and
  2. subscription events tagged student:s1.

Factos then supports this flow:

select relevant events
        |
        v
fold them into temporary decision state
        |
        v
run a pure business decision
        |
        v
append new events only if no matching event appeared meanwhile

The accepted fact can be one event:

StudentSubscribedToCourse(student: s1, course: c1)

It carries both student:s1 and course:c1 tags. Future commands can find the same fact from either point of view. There is no need to record two copies of one business fact merely because two concepts are involved.

This is the core idea behind Dynamic Consistency Boundaries. “Dynamic” means the protected set is chosen for each command. “Consistency boundary” means a concurrent change to that set invalidates the decision before its events can be committed.

What each layer owns

ConcernYour applicationFactos
Business languageCommands, events, errors, state namesNo generic domain vocabulary
Business rulesPure try and evolve functionsModel and dispatch flow
Relevant historyA DecisionContext per commandMatching semantics and append condition
Persistence formatJSON encoders, version-aware decoders, stable type names and tagsEvent envelope and recorded-event types
StorageBackend choice and connection lifecyclePostgreSQL, SQLite, and Cloudflare D1 adapters
Read modelsProjection schema and update logicTransactional subscription hooks where supported
TestingScenarios and expected domain behaviorStore-free simulator and backend projection simulators

Factos deliberately does not own your domain model. There is no aggregate base class, repository interface, global command bus, projection framework, HTTP layer, or message broker hidden behind the API.

The five pieces of a model

1. Commands ask for change

A command is an intent, usually named in the imperative:

pub type Command {
  SubscribeStudentToCourse(student_id: String, course_id: String)
}

A command can be rejected. It is not part of the permanent history just because someone requested it.

2. Events record accepted facts

An event is a past-tense fact that the domain accepted:

pub type Event {
  StudentSubscribedToCourse(student_id: String, course_id: String)
}

Events are the authoritative history. Their stored type names, versions, payloads, and selection tags form a long-lived data contract.

3. A model contains the rule

A Model has three pure decision parts:

The state is a calculation aid, not a stored mutable domain object. A backend may retry the transaction, so decision code must be deterministic and perform no IO.

4. A decision context names relevant facts

Factos provides three context shapes:

Use the narrowest context that fully protects the business invariant. Narrow contexts allow unrelated commands to proceed independently.

5. A backend makes the decision safe

The core factos package is store-independent. A backend reads the context, applies the model’s decision functions, checks the append condition, commits the accepted event batch, and returns globally ordered Recorded events.

PackageUse it for
factosDomain model, event envelope, context types, and simulation
factos_pogPostgreSQL with serializable dispatch transactions
factos_sqlightSQLite through Sqlight
factos_cfCloudflare D1 transaction plans

Backend guarantees and transaction APIs differ. Read the selected package’s API documentation before choosing where projections or other transactional work runs.

What value does this provide?

Business rules stay visible

The command, relevant facts, decision, accepted events, and domain errors remain ordinary application types and pure functions. Storage mechanics do not become the domain language.

Consistency follows the rule

A command can protect facts about one entity, several entities, or a unique value without forcing every command through the same coarse boundary. This is particularly useful for uniqueness, capacity, quotas, reservations, and other cross-entity invariants.

One fact can serve several viewpoints

Tags let a single event participate in multiple future decisions. Projections can independently turn the same history into query-oriented views.

Domain scenarios run without infrastructure

factos/simulate can add existing events, dispatch real commands through the real model and codecs, and inspect accepted events or errors. A database is still required to prove a backend’s transaction and concurrency guarantees.

Storage remains replaceable at the model boundary

The core model does not depend on a database connection. Backends share the same commands, events, decisions, codecs, and contexts while exposing their actual transaction semantics rather than pretending every store behaves identically.

When Factos is a good fit

Factos is worth considering when:

It is probably unnecessary when a CRUD model already expresses the domain well, history has no business or operational value, or eventual consistency is acceptable for every cross-record rule. Event Sourcing adds schema evolution, projection recovery, storage growth, and operational responsibilities; Factos does not make those costs disappear.

A practical learning path

  1. Read Domain-Driven Design: a practical primer for the modelling vocabulary.
  2. Read Event Sourcing and command dispatch for the read-decide-append lifecycle.
  3. Read Dynamic Consistency Boundaries for the concurrency model and the course example.
  4. Use The Factos core model as the API-oriented reference.
  5. Run one of the repository’s DCB examples, then test the same model through the backend you intend to deploy.
Search Document