☝️🤓 Factos
Factos is a context-first Event Sourcing library for Gleam. It lets a business decision read and protect the exact facts that can change its answer, even when those facts span several entities or would traditionally live in different event streams.
command + relevant accepted facts -> new facts or a domain error
Applications own their domain language, decisions, event codecs, projections, and effects. Factos provides:
- a small, store-independent model for pure decisions;
- Dynamic Consistency Boundary predicates built from event types and tags;
- optimistic append conditions over those predicates;
- deterministic domain and codec simulation;
- PostgreSQL, SQLite, and Cloudflare D1 storage adapters; and
- transactional subscription hooks where a backend supports them.
Factos is not a DDD framework, command bus, projection framework, or generic application architecture.
Why it exists
Business invariants often cross fixed storage boundaries. A course subscription, for example, may depend on the course’s capacity, the student’s existing subscriptions, and whether that student already joined that course.
A traditional stream-per-aggregate design can protect either stream with an expected revision, but protecting the combined decision requires extra coordination. Factos instead makes the relevant set of facts explicit for each command:
read matching facts
-> fold temporary decision state
-> apply a pure decision
-> append only if no matching fact appeared meanwhile
This is Dynamic Consistency Boundaries (DCB): consistency follows the business rule rather than a permanent stream boundary. Unrelated commands can still use different, narrower contexts.
Start with the concepts
The documentation is ordered for readers who are new to the ideas:
- Start here: why Factos exists
- Domain-Driven Design: a practical primer
- Event Sourcing and command dispatch
- Dynamic Consistency Boundaries
- The Factos core model
Core model
A Model combines pure state transition and decision functions with the
application’s event codec:
let model =
factos.model(
initial: initial_state,
try: try_command,
evolve: evolve,
encode: encode_event,
decode: decode_event,
)
See The Factos core model for complete definitions of all five model values.
Every dispatch also supplies a DecisionContext:
NoContextreads no history and appends unconditionally;Matching(items:)selects facts by event type and tags;AllEventsreads and protects the complete event log.
Items are OR-combined. Types inside an item are OR-combined; tags are AND-combined.
Simulate domain scenarios
The core simulator exercises the real model and codecs without a database:
let simulation =
simulate.new(model, with: [
TicketSold(buyer: "renata"),
])
|> simulate.dispatch(
decision_context: sale_context(),
command: BuyTicket(buyer: "lucy"),
)
|> simulate.tap(fn(simulation) {
assert simulate.events(simulation)
== Ok([
TicketSold(buyer: "renata"),
TicketSold(buyer: "lucy"),
])
})
assert simulate.errors(simulation) == []
Simulation proves domain folding, decisions, errors, and codec behavior. It does not prove an event store’s transaction isolation or concurrency guarantee; use backend dispatch integration tests for those.
PostgreSQL and SQLite projection simulators can run the application’s real subscriptions against a projection database while keeping event history in memory. Each accepted batch gets its own backend transaction, and a failed subscription rolls back that batch’s projection writes.
Dispatch through a backend
Configuration and dispatch are backend-specific:
let configuration =
factos_pog.configure(model, connection: connection)
configuration
|> factos_pog.dispatch(
command,
decision_context: decision_context(command),
event_id: new_event_id,
)
factos_pog runs dispatches in serializable PostgreSQL transactions and retries
serialization or deadlock conflicts. SQLite and Cloudflare D1 expose the same
core model through storage-appropriate transaction APIs.
Packages
| Package | Role |
|---|---|
factos | Store-independent model, event envelope, contexts, and simulator |
factos_pog | PostgreSQL event store and transactional subscriptions |
factos_sqlight | SQLite event store through Sqlight |
factos_cf | Cloudflare D1 event store and immutable transaction plans |
Runnable DCB examples live under
examples/. They cover course
subscriptions, unique usernames, invoice numbers, opt-in tokens, dynamic
product prices, and record deduplication.
Development
trellis run check
trellis run test
PostgreSQL-backed tests and the benchmark use the root Compose service:
docker compose up --wait -d
See the changelog for release history.