# 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 by
`sqlx` + `tokio`; the only dependency is Postgres. **This is the focus of
this README.**
- **[`mire-sagas`](crates/mire-sagas)** — a saga / process-manager layer
*on top* of `mire`, for workflows that span several aggregates (reserve →
pay → confirm, with compensation and exactly-once effects). Optional —
[see below](#sagas).
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`](docs/ARCHITECTURE.md); for the operator's checklist of
what *you* must get right, see [`docs/SUCCESS.md`](docs/SUCCESS.md).
## Install
```toml
[dependencies]
mire = "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
```rust
use mire::{Aggregate, EventData, EventStore};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
#[derive(Debug, Clone, Serialize, Deserialize, EventData)]
#[serde(tag = "type")]
#[mire(entity = "account")]
enum AccountEvent {
Opened { owner: String },
Deposited { amount: i64 },
}
#[derive(Debug, Default)]
struct Account {
owner: String,
balance: i64,
}
impl Aggregate for Account {
type Event = AccountEvent;
fn stream_category() -> &'static str { "account" }
fn apply(&mut self, event: &AccountEvent) {
match event {
AccountEvent::Opened { owner } => self.owner = owner.clone(),
AccountEvent::Deposited { amount } => self.balance += amount,
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let pool = PgPool::connect("postgres://…").await?;
let store = EventStore::new(pool);
store.migrate().await?; // once at startup
let mut account = store.load_or_default::<Account>("acc-123").await?;
account.record(AccountEvent::Opened { owner: "Ada".into() });
account.record(AccountEvent::Deposited { amount: 100 });
store.save(&mut account).await?;
let reloaded = store.load::<Account>("acc-123").await?.unwrap();
assert_eq!(reloaded.state.balance, 100);
Ok(())
}
```
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.
| **Aggregate lifecycle** | The minimum shape. Defining events, an aggregate, load/record/save. Every other pattern builds on this. | [`bank_account`](crates/mire/examples/bank_account.rs) |
| **Commands & validation** | Refusing invalid operations *before* recording events. The right place for "can't overdraft", "can't operate on a closed account", etc. | [`commands`](crates/mire/examples/commands.rs) |
| **Concurrency conflicts** | Two writers race on the same aggregate. mire uses optimistic concurrency — no locks; the loser retries. | [`concurrency`](crates/mire/examples/concurrency.rs) |
| **Projections (read models)** | Aggregates are great for writing; terrible for querying. Build a query-optimised table the runner keeps current. | [`projection`](crates/mire/examples/projection.rs) |
| **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`](crates/mire/examples/snapshot.rs) |
| **Read-before-write across aggregates** | One business decision spans two aggregates (e.g. atomic money transfer). The escape hatch — used sparingly. | [`transaction_scope`](crates/mire/examples/transaction_scope.rs) |
| **Multi-replica deployment** | Production runs multiple replicas. Exactly one drives each projection at a time, with automatic failover. | [`multi_replica`](crates/mire/examples/multi_replica.rs) |
| **High-throughput writes** | Bulk ingest. Calling `save` once per event is 10× slower than necessary; batch them. | [`batched_write`](crates/mire/examples/batched_write.rs) |
## Running the examples
```sh
mise run pg:up # starts a local Postgres on :5434 via docker compose
cargo run --example bank_account
cargo run --example commands
# … etc.
```
If you don't use `mise`:
```sh
docker compose up -d postgres
export DATABASE_URL=postgres://mire:mire@localhost:5434/mire
cargo run --example bank_account
```
## 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`](crates/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** `decide` that folds state into a typed request
(it can't perform IO, so it's always safe to re-run on recovery), and
- an `async` **`effect`**, 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).
```toml
[dependencies]
mire-sagas = "0.2"
```
See the [`mire-sagas` README](crates/mire-sagas/README.md) for the model and a
fully-typed example, plus the runnable
[`travel_booking`](crates/mire-sagas/examples/travel_booking.rs) (minimal,
in-process effects) and
[`travel_booking_service`](crates/mire-sagas/examples/travel_booking_service.rs)
(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](./LICENSE-MIT)