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:
- a course cannot exceed its capacity;
- a student cannot subscribe to the same course twice;
- a student cannot subscribe to more than five courses.
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:
- course-definition, capacity-change, and subscription events tagged
course:c1; and - 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
| Concern | Your application | Factos |
|---|---|---|
| Business language | Commands, events, errors, state names | No generic domain vocabulary |
| Business rules | Pure try and evolve functions | Model and dispatch flow |
| Relevant history | A DecisionContext per command | Matching semantics and append condition |
| Persistence format | JSON encoders, version-aware decoders, stable type names and tags | Event envelope and recorded-event types |
| Storage | Backend choice and connection lifecycle | PostgreSQL, SQLite, and Cloudflare D1 adapters |
| Read models | Projection schema and update logic | Transactional subscription hooks where supported |
| Testing | Scenarios and expected domain behavior | Store-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:
initialis the temporary empty decision state;evolvefolds relevant historical events into that state;tryaccepts the state and command, then returns new events or a domain error.
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:
NoContextwhen no prior event can change the answer;Matching(items:)for selective event-type and tag predicates;AllEventswhen every event really is relevant.
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.
| Package | Use it for |
|---|---|
factos | Domain model, event envelope, context types, and simulation |
factos_pog | PostgreSQL with serializable dispatch transactions |
factos_sqlight | SQLite through Sqlight |
factos_cf | Cloudflare 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:
- accepted business facts are valuable as an audit and decision history;
- important rules span concepts that do not fit one fixed stream boundary;
- you want pure, deterministic domain decisions in Gleam;
- you can design and maintain event schemas and projections deliberately; and
- your chosen backend can provide the consistency required by those rules.
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
- Read Domain-Driven Design: a practical primer for the modelling vocabulary.
- Read Event Sourcing and command dispatch for the read-decide-append lifecycle.
- Read Dynamic Consistency Boundaries for the concurrency model and the course example.
- Use The Factos core model as the API-oriented reference.
- Run one of the repository’s DCB examples, then test the same model through the backend you intend to deploy.