This is the strongest Statum example in the repo because it shows the part that is hard to fake with normal status enums: rebuilding a workflow from append-only events without dropping back to ad hoc runtime branching.
It is also the clearest representational-correctness story in the repo: raw projected facts do not become ordinary workflow values until they can be proven to match one legal state.
Source:
- runnable example: statum-examples/src/showcases/sqlite_event_log_rebuild.rs
- deeper validator guide: persistence-and-validators.md
- related patterns: patterns.md
You have an append-only event log:
createdpaidpackedshippeddelivered
You need to answer questions like:
- what state is this order in now?
- which operations are legal next?
- which fields are only valid after specific events?
The usual runtime shape is:
- reduce events into a row or snapshot
- carry a status string or enum
- branch on that status later
- hope the data attached to that status is still consistent
That works, but the legal workflow still lives in runtime code. Undesirable or inconsistent states can still travel as ordinary values.
The example splits the problem into three explicit layers:
#[state]declares the legal phases:Created,Paid,Packed,Shipped,Delivered.statum::projectionreduces the event stream into one projection row per order.#[validators(OrderMachine)]rebuilds that projection into a typed machine.
Once rebuilt, the result is not "an order plus a status field." It is one of:
order_machine::SomeState::Createdorder_machine::SomeState::Paidorder_machine::SomeState::Packedorder_machine::SomeState::Shippedorder_machine::SomeState::Delivered
That matters because the workflow boundary is no longer implicit. The type system now distinguishes legal states from rows that merely resemble them.
The example only defines legal edges:
Created -> PaidPaid -> PackedPacked -> ShippedShipped -> Delivered
After reconstruction, you work with the concrete typed machine. If you are in
Packed, the next legal operation is ship(...). There is no normal method
for skipping ahead to deliver() from the wrong state.
The projection row may carry optional fields like payment_receipt,
pick_ticket, and tracking_number, but the rebuilt machines do not expose
them uniformly.
Instead:
Paidcarriespayment_receiptPackedcarriespayment_receiptandpick_ticketShippedcarriestracking_numbertoo
That makes the type system reflect the event history you actually observed. Fields stop existing in phases where they would only be guesses.
Without Statum, it is common to reduce events into a snapshot and then keep rechecking status everywhere else in the code.
In this example, projection is one step and typed rebuild is the next step:
let row = projection::reduce_one(events, &OrderProjector)?;
let state = row.into_machine().build()?;After that, downstream code works on typed states rather than repeating snapshot-to-workflow interpretation logic. That is the handoff from unvalidated facts to legal domain states.
The same example uses .into_machines() after grouped projection so many
orders can be reconstructed in one pass.
That is useful because append-only systems rarely rebuild just one workflow at a time.
This example fits Statum well because the workflow has:
- a stable set of legal phases
- expensive invalid transitions
- state-specific data that should not exist everywhere
- a persistence boundary where runtime facts must be turned back into a legal workflow shape
That is the kind of problem where typestate removes a real class of mistakes.
Statum is not replacing event storage, projections, or orchestration. The example still has an explicit projector and explicit persistence code.
What Statum owns is the workflow boundary after projection:
- are these facts a legal
Createdorder? - or
Paid? - or
Packed? - and once rebuilt, which transitions are legal next?
That is the part that would otherwise decay into scattered runtime checks.
- README for the quick mental model
- statum-examples/src/showcases/sqlite_event_log_rebuild.rs for the full runnable example
- persistence-and-validators.md for the rebuild APIs
- patterns.md for adjacent usage patterns