# ☝️🤓 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.