Folds
A fold is a “reduce” over the event log. You declare which events to read and how each one updates a piece of in-memory state. Commands replay folds on every call to recover whatever state they need to make a decision: they have no SQLite, no other query path.
If you’ve used Iterator::fold / Array.reduce, the mental model is identical:
// Iterator::fold
events.iter().fold(State::default(), |state, event| apply(state, event));
// Umari Fold trait
fn apply(&self, state: &mut Self::State, event: StoredEvent<E>) { ... }
The runtime supplies the events (scoped by the fold’s domain IDs) and the initial state (State::default()). You only write the body.
Defining a fold
A fold is a struct implementing the Fold trait:
pub trait Fold: DomainIds + 'static {
type Events: EventSet;
type State: Default + 'static;
fn apply(&self, state: &mut Self::State, event: StoredEvent<<Self::Events as EventSet>::Item>);
}
Events: which events this fold subscribes to (anEventSet)State: the type of state produced by replaying those events (must implementDefault)apply(): called once per matching event, in position order, to update the state
Simple fold
A fold that records whether a user exists, scoped by user_id:
use umari::prelude::*;
#[derive(DomainIds, FromDomainIds)]
pub struct UserExistsFold {
#[domain_id]
pub user_id: u64,
}
impl Fold for UserExistsFold {
type Events = SingleEvent<UserRegistered>;
type State = bool;
fn apply(&self, exists: &mut bool, _event: StoredEvent<UserRegistered>) {
*exists = true;
}
}
The state starts as false (the Default for bool). When a UserRegistered event is encountered during replay, the state becomes true. The command can then check if !exists { ... }.
Fold over multiple event types
For folds that need more than one event type:
Group them in an EventSet enum:
#[derive(EventSet)]
pub enum UserEmailQuery {
UserRegistered(UserRegistered),
UserReactivated(UserReactivated),
}
#[derive(DomainIds, FromDomainIds)]
pub struct UserEmailFold {
#[domain_id]
pub user_id: u64,
}
impl Fold for UserEmailFold {
type Events = UserEmailQuery;
type State = Option<String>;
fn apply(&self, email: &mut Option<String>, event: StoredEvent<UserEmailQuery>) {
match event.data {
UserEmailQuery::UserRegistered(ev) => *email = Some(ev.email),
UserEmailQuery::UserReactivated(ev) => *email = Some(ev.email),
}
}
}
The apply body receives the typed event and decides how to update state. Events arrive in position order; the state after all events have been applied is what’s handed to the command’s execute.
Built-in fold types
Umari ships several generic folds for common patterns, identical in both SDKs. In Rust you name the type (cmd.fold::<EventFold<E>>()); in TypeScript you call the helper and bind it (EventFold(E)({ … })).
EventFold
Collects ALL occurrences of event E. Use when you need the full history.
let projects = cmd.fold::<EventFold<ProjectCreated>>();
// State: EventState<ProjectCreated> { events: Vec<StoredEvent<ProjectCreated>> }
// projects.exists() → true if at least one event exists
LatestEvent
Keeps only the most recent occurrence of event E. More efficient than EventFold when you only need the current value.
let latest = cmd.fold::<LatestEvent<ProjectCreated>>();
// State: Option<StoredEvent<ProjectCreated>>
EventCounter
Counts occurrences of event E. Efficient: it doesn’t retain events.
let task_count = cmd.fold::<EventCounter<TaskCreated>>();
// State: u64
EventToggle
Tracks which of two opposing events occurred last. Ideal for created/deleted, activated/deactivated, archived/unarchived pairs.
let toggle = cmd.fold::<EventToggle<ProjectArchived, ProjectUnarchived>>();
// State: ToggleState<A, B> {
// last: Option<ToggleSide<A, B>> // None, Some(ToggleSide::A(...)), or Some(ToggleSide::B(...))
// }
Custom folds
For anything beyond the built-in types, build a state object from several event types, scoped to one entity:
#[derive(Default)]
pub struct ProjectState {
pub exists: bool,
pub title: Option<String>,
pub status: ProjectStatus,
pub archived: bool,
}
#[derive(EventSet)]
pub enum ProjectEvents {
#[scope(project_id)]
ProjectCreated(ProjectCreated),
#[scope(project_id)]
ProjectUpdated(ProjectUpdated),
#[scope(project_id)]
ProjectArchived(ProjectArchived),
#[scope(project_id)]
ProjectUnarchived(ProjectUnarchived),
}
#[derive(DomainIds, FromDomainIds)]
pub struct ProjectFold {
#[domain_id]
pub project_id: Uuid,
}
impl Fold for ProjectFold {
type Events = ProjectEvents;
type State = ProjectState;
fn apply(&self, state: &mut ProjectState, event: StoredEvent<ProjectEvents>) {
match event.data {
ProjectEvents::ProjectCreated(ev) => {
state.exists = true;
state.title = Some(ev.title);
state.status = ev.status;
}
ProjectEvents::ProjectUpdated(ev) => {
state.title = Some(ev.title);
state.status = ev.status;
}
ProjectEvents::ProjectArchived(_) => state.archived = true,
ProjectEvents::ProjectUnarchived(_) => state.archived = false,
}
}
}
#[scope(project_id)] ensures we only see events for the specific project being queried. Without the scope attribute, the fold would filter by all domain ID bindings from the command input, which may be too narrow.
Registering folds in commands
A command names the folds it needs; the runtime replays each one and hands the terminal states to execute. Folds run before your logic, in a single DCB query.
Commands register folds through the builder pattern:
Command::new(input, context)
.fold::<UserExistsFold>() // No extra args
.fold::<ProjectFold>() // No extra args
.fold_args::<CustomFold>(args) // With additional constructor args
.fold_with(|input| MyFold { ... }) // Manual construction from input
.execute(|input, (user_exists, project_state)| {
// ...
})
Each .fold::<T>() call extends the DCB query with the fold’s event domain IDs, creates the fold from the input’s bindings (via FromDomainIds), initializes its state to Default, and returns a FoldHandle<T>. The execute closure receives the input and a tuple of fold states, in registration order. Up to 12 folds are supported in a single tuple.
Fold state and idempotency
When the runtime replays events into folds, it also checks for idempotency. If the command was called with an idempotency_key, and any event in the fold’s scope has a matching key, the command exits early without running your logic, returning an empty result. This means you can safely retry command executions without worrying about duplicate events; the runtime deduplicates at the event store level.
Crypto-shredded events in folds
When an event’s encryption key has been deleted, its payload reads back as null with an encryption scope set. Folds skip these events silently (apply is never called for them), so your fold state simply won’t reflect the shredded event, which is the intended behavior.
Standalone fold execution
Outside of commands, you can run folds directly against the event store. This is useful in effects for checking event store state before performing a side effect.
use umari::prelude::FoldQuery;
let registered = FoldQuery::new()
.fold_iter(topics.iter().map(|topic| AlreadyRegisteredFold {
user_id,
topic: topic.to_string(),
current_event_id: event.id,
}))
.run()?;
It opens a transaction, reads events, applies them to the bound folds, and returns the terminal states, the same mechanism commands use internally, exposed for effects that need to check event store state without executing a full command.