How factos_pog Works
factos_pog is a PostgreSQL event-store backend for the Factos core model.
It implements two write paths:
dispatch_with_query: protect an arbitrary Factos event-type/tag context;dispatch: protect one stream revision.
It persists event records only. Views are computed by application folds over events, and durable materialized views are application-owned tables or stores. Reactors produce effect values, but this package does not execute or retry those effects.
Both paths return Dispatch(event), which includes append metadata and the
committed factos.Recorded(event) values inserted by the dispatch.
Event rows
The append-only table is factos_events:
| Column | Meaning |
|---|---|
position | Global append order. |
id | Application event id. |
stream | Stream name. |
revision | Per-stream revision. |
type | Store-visible event type name. |
version | Event version from the application codec. |
tags | Newline-encoded store-visible tags. |
metadata | Newline-encoded application metadata. |
data | Opaque application bytes. |
Tags are also mirrored into factos_event_tags(position, tag). This table gives
PostgreSQL indexed predicates for context queries.
Context dispatch flow
dispatch_with_query is for commands whose consistency boundary is a
factos.Query.
The transaction does this:
- lock
factos_eventsin exclusive mode; - select candidate rows with SQL generated from the query;
- decode candidate rows using the application codec;
- apply
factos.matches_querysemantics; - fold events with the decider’s
evolvefunction; - run the decider with the command;
- check
FailIfEventsMatch(query, after); - insert produced events;
- insert tag-index rows;
- return
Dispatch(event).
The table lock is intentionally conservative. It is the simplest way to make the append condition correct for arbitrary query shapes.
Stream dispatch flow
dispatch is for commands whose consistency boundary is one stream.
The backend:
- loads the stream ordered by revision;
- decodes rows;
- folds the stream state;
- runs the decider;
- checks that the stream revision still matches;
- inserts the produced events;
- returns
Dispatch(event).
Use stream dispatch only when the business rule really is protected by one
stream. If the rule needs event types and tags across streams, use
dispatch_with_query.
Codec boundary
The backend never interprets event payload bytes. The application codec decides:
- how to encode event payloads;
- which event type name to store;
- which tags to expose for future queries;
- how to decode old stored rows;
- how to handle unknown or invalid data.
This keeps the backend generic and makes the query contract visible at write time.
Reactors and effects
dispatch.events contains committed records, not merely domain events. Records
include id, stream, revision, global position, type, version, tags, metadata, and
payload.
That makes them suitable for factos.Reactor values:
let effects = factos.react_all(ticket_reactor(), dispatch.events)
factos_pog does not execute those effects. If effects need retry or durability,
model that in application code or a backend-specific adapter.