How factos_pog Works

factos_pog is the PostgreSQL event-store and durable-delivery backend for Factos. A bounded context constructs one validated Dispatcher containing its event codec and complete list of named subscriptions. Construction rejects blank and exact duplicate subscription names before database IO.

Dispatch flow

new_dispatch creates command-specific consistency configuration around that dispatcher. Without with_query, one stream revision is the consistency boundary. With with_query, the boundary is an arbitrary Factos event-type/tag query.

One PostgreSQL SERIALIZABLE transaction:

  1. reads and decodes the selected event context;
  2. folds the context with the decider’s evolve function;
  3. runs the decider;
  4. verifies the observed stream or query context is still current;
  5. assigns event ids and inserts produced events and tag-index rows;
  6. runs every configured pure subscription reactor;
  7. encodes and inserts the resulting outbox effects;
  8. commits events and effects atomically.

Serialization and deadlock conflicts retry up to the builder’s configured attempt count. Deciders, codecs, reactors, and event-id generators can therefore run more than once and must remain pure or idempotent.

Event storage

factos_events is append-only. It stores global position, application event id, stream revision, type, version, tags, metadata, and opaque payload bytes. factos_event_tags(position, tag) mirrors tags into indexed rows used by context queries.

Application codecs own payload encoding and decoding. new_proposed requires the typed event, durable type, schema version, and payload, while defaulting tags to [] and metadata to factos.empty_metadata(). Codecs optionally replace those values through with_tags and with_metadata. Any value needed by a future selective consistency query must be exposed as an event tag.

After validating a stored type and decoding its payload, return decoded_event(stored, event:). It creates the successful codec result while preserving the stored type, version, tags, and metadata.

Durable subscriptions

A Subscription combines:

Every configured subscription observes every newly committed event and may return no effects. The subscription name is persisted separately and also namespaces effect_key, so heterogeneous subscriptions can safely share one dispatcher and outbox.

Leasing and settlement

An OutboxConsumer binds one consumer/target identity to one validated RetryPolicy. Policies start from documented defaults, accept focused builder overrides, and validate only when built. Leasing selects only pending, available rows for that identity, increments attempt, and returns an unguessable lease token.

The worker classifies each attempt with one opaque input value:

settle_outbox performs one lease-guarded update. Retryable failures use capped exponential backoff with deterministic jitter. They become dead letters when either the maximum attempt count or maximum delivery age is reached. Permanent failures dead-letter immediately. Expired, superseded, or mismatched leases return StaleLease without changing the row.

Applications own failure classification and effect execution. The three constructors classify one leased message; they do not control a worker actor. Applications do not own retry delay calculation, retry budgets, or dead-letter transitions.

Operational recovery

list_dead_letters returns safe envelope metadata and replay history. replay_dead_letter moves a dead letter back to pending while preserving its original payload. Operators explicitly choose whether to preserve or reset the attempt count; replay count and timestamp remain recorded for audit.

Go deeper

Search Document