mire 0.2.4

A small, generic PostgreSQL event-sourcing library: append-only event streams, aggregates with optimistic concurrency, and subscription-based projections (requires tokio + sqlx)
Documentation
# 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.

| 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`]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)