Current section
Files
Jump to
Current section
Files
README.md
# ☝️🤓 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.
```text
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:
```text
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:
1. [Start here: why Factos exists](docs/start-here.md)
2. [Domain-Driven Design: a practical primer](docs/domain-driven-design.md)
3. [Event Sourcing and command dispatch](docs/event-sourcing.md)
4. [Dynamic Consistency Boundaries](docs/dynamic-consistency-boundaries.md)
5. [The Factos core model](docs/core-model.md)
## Core model
A `Model` combines pure state transition and decision functions with the
application's event codec:
```gleam
let model =
factos.model(
initial: initial_state,
try: try_command,
evolve: evolve,
encode: encode_event,
decode: decode_event,
)
```
See [The Factos core model](docs/core-model.md#model) for complete definitions of
all five model values.
Every dispatch also supplies a `DecisionContext`:
- `NoContext` reads no history and appends unconditionally;
- `Matching(items:)` selects facts by event type and tags;
- `AllEvents` reads 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:
```gleam
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:
```gleam
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/`](https://tangled.org/renatillas.dev/factos/tree/main/examples). They cover course
subscriptions, unique usernames, invoice numbers, opt-in tokens, dynamic
product prices, and record deduplication.
## Development
```sh
trellis run check
trellis run test
```
PostgreSQL-backed tests and the benchmark use the root Compose service:
```sh
docker compose up --wait -d
```
See the
[changelog](https://tangled.org/renatillas.dev/factos/blob/main/CHANGELOG.md)
for release history.