Repository

The EventStore façade — load and save aggregates against a real store, one terminal for owning and zero-copy codecs.

Repository

The repository is the typed front door to persistence. Where the raw store moves bytes, the repository moves aggregates: load rehydrates an AggregateRoot<A> from its stream, and save persists decided events and folds them back in. It ties together the store, a codec, and (optionally) an upcaster and a snapshot store.

Building one

Start from a Store<S> handle, name the aggregate once, choose a codec, and build:

rust
let store = FjallStore::builder(path).open()?.into_store();
let repo  = store.repository::<BankAccount>().json().build();
  • .into_store() wraps any RawEventStore in the clone-cheap Store<S> handle (the multi-aggregate substrate).
  • .repository::<BankAccount>() names the aggregate once — from here on load/save infer it, no per-call type annotations.
  • .json() picks the built-in JsonCodec; .codec(my_codec) plugs in any Encode/Decode pair.
  • .build() produces an EventStore<S, C, A> — the Repository<A> impl.

Load and save

rust
// Rehydrate: read the stream, decode each event, replay in version order.
let mut account = repo.load(id.clone()).await?;

// Decide on the loaded aggregate:
let decided = account.handle(Deposit { amount: 100 })?;

// Persist decided events and fold them back in (advances the version):
repo.save(&mut account, &decided).await?;

load does the whole rehydration dance — open the stream, decode with the configured codec, replay each event — and hands you a ready aggregate. save appends the decided events (version-checked, so a concurrent writer is rejected) and then commit_persisteds them into your in-memory root. The two error domains stay distinct: store errors (I/O, conflict) versus codec errors (decode failures).

One façade for owning and zero-copy codecs

EventStore is a single terminal for both kinds of codec — there is no separate "zero-copy repository." The trick is that the owning-vs-borrowing distinction is inferred from the Decode::Output GAT, not restated:

  • an owning codec (serde, Output = E) works — one allocation per event;
  • a zero-copy codec (rkyv/bytemuck, Output = &E) works — no allocation.

Both are fed to replay via out.borrow(), consumed in place. You never spell the bound; the builder and load/save handle it.

CommandRepository — decide + save in one call

For the common load → decide → save cycle, the CommandRepository<A> extension trait (blanket-impl'd on every repository) offers execute, so you don't hand-write the three steps each time. Use Repository::load/save when you need the steps apart (e.g. inspecting decided events before persisting).

Snapshots, transparently

Add .snapshot_store(..) (or .snapshot_store_json(..)) before .build() and the repository is wrapped in a Snapshotting decorator: it hydrates from a snapshot on load and commits one per a PersistTrigger on save — with no change to your call sites. See Snapshots.

Where to next