mire
A small PostgreSQL event-sourcing system for Rust, in two layers:
mire— the event store. Append-only event streams, optimistic concurrency, snapshots, replica-safe projections via lease + fence-token, and an escape hatch for the rare read-before-write case. Backed bysqlx+tokio; the only dependency is Postgres. This is the focus of this README.mire-sagas— a saga / process-manager layer on top ofmire, for workflows that span several aggregates (reserve → pay → confirm, with compensation and exactly-once effects). Optional — see below.
The two compose: a saga is itself just a mire aggregate (its state is an
event stream), so everything you learn about the event store below carries
over.
For the whole picture — the life of an event and of a saga, the guarantees and
where they stop, where things fail and how they recover — see
docs/ARCHITECTURE.md; for the operator's checklist of
what you must get right, see docs/SUCCESS.md.
Install
[]
= "0.2"
Pre-1.0 — the API can shift between minor releases. Pin a patch
(= "0.2.4") if you need stability across the next change.
Quickstart
use ;
use ;
use PgPool;
async
That's the shape of every interaction with mire: load → record → save, and load again to replay state from the log.
Patterns
Each example below is a self-contained slice — one concept, one runnable program, a short "why this pattern exists" block at the top. Pick the one matching what you're trying to do.
| Pattern | When to use | Example |
|---|---|---|
| Aggregate lifecycle | The minimum shape. Defining events, an aggregate, load/record/save. Every other pattern builds on this. | bank_account |
| Commands & validation | Refusing invalid operations before recording events. The right place for "can't overdraft", "can't operate on a closed account", etc. | commands |
| Concurrency conflicts | Two writers race on the same aggregate. mire uses optimistic concurrency — no locks; the loser retries. | concurrency |
| Projections (read models) | Aggregates are great for writing; terrible for querying. Build a query-optimised table the runner keeps current. | projection |
| Snapshots | A single aggregate has accumulated >1k events and load is on a hot path. Cache the folded state so loads stay O(1). | snapshot |
| Read-before-write across aggregates | One business decision spans two aggregates (e.g. atomic money transfer). The escape hatch — used sparingly. | transaction_scope |
| Multi-replica deployment | Production runs multiple replicas. Exactly one drives each projection at a time, with automatic failover. | multi_replica |
| High-throughput writes | Bulk ingest. Calling save once per event is 10× slower than necessary; batch them. |
batched_write |
Running the examples
# … etc.
If you don't use mise:
Sagas
When a single business process spans several aggregates — reserve
inventory, capture payment, issue a ticket, and roll all of it back if any
step fails — that orchestration belongs in no single aggregate.
mire-sagas is a Postgres-backed process manager for
exactly that, built on the event store above.
A saga is a declarative DAG of steps. Each step splits into two halves:
- a pure, synchronous
decidethat folds state into a typed request (it can't perform IO, so it's always safe to re-run on recovery), and - an
asynceffect, co-located via.effect(...), that does the IO.
On top of that the runner gives you compensations (automatic rollback in reverse order), timeouts, external signals, sharded scale-out across replicas, and exactly-once effects under crashes and concurrency (via an instance lease + OCC + idempotency keys).
[]
= "0.2"
See the mire-sagas README for the model and a
fully-typed example, plus the runnable
travel_booking (minimal,
in-process effects) and
travel_booking_service
(real providers over HTTP, durable outbox, chaos-tested) examples.
What mire is not
- Not a queue. Events go into a per-stream log, not a Kafka-style topic. Use mire when your durable state IS your event log.
- Not multi-region. Single Postgres cluster, with the usual Postgres-replication options for read replicas / DR.
- Not a CRUD ORM. State lives in events; aggregates are
reconstructed by replay. If you want to
UPDATE … SET …, use sqlx directly.
License
Licensed under MIT