Skip to content

Examples

The food-delivery order saga.

Crucible’s flagship example is a food-delivery order saga, examples/fooddelivery: a generic ordering lifecycle invented from scratch, coupled to no real product. It is a single statechart that exercises the whole engine the way a real service would: hierarchy, parallel regions, guards authored as data, actors, invoked services with compensation, and timer-driven escalation.

The diagram below is generated at build time from the real fooddelivery.NewModel() machine (the same machine the example’s tests exercise), so it can never drift from the code:

stateDiagram-v2
    [*] --> Placed
    state Active {
        [*] --> Active_Fulfillment__Cooking
        Active_Fulfillment__Cooking
        Active_Fulfillment__AwaitingCourier
        Active_Fulfillment__EnRoute
        Active_Fulfillment__AwaitingCourier --> Active_Fulfillment__EnRoute: PickedUp
        Active_Fulfillment__Cooking --> Active_Fulfillment__AwaitingCourier: PlatedUp
        --
        [*] --> Active_Watchdog__OnTime
        Active_Watchdog__OnTime
        Active_Watchdog__Overdue
        Active_Watchdog__Overdue --> [*]
        Active_Watchdog__OnTime --> Active_Watchdog__Overdue: after(30m0s) SLABreached
    }
    Delivered --> [*]
    Canceled --> [*]
    Rejected --> [*]
    Active --> Refunding: Cancel
    Active --> Settling: DroppedOff
    Authorizing --> Active: Authorized [or(generousOrder,and(subtotal ge 5000,priority in ["fast","express"]))]
    Authorizing --> Rejected: Declined
    Placed --> Authorizing: Submit
    Refunding --> Canceled: Refunded
    Settling --> Delivered

An order rests in Placed. A Submit signal moves it to Authorizing, which invokes a payment service to hold funds. The authorization’s outcome routes through two edges: Authorized advances the order, Declined sends it to the Rejected terminal.

The Authorized edge is no rubber stamp. Its guard is an expression mixing tiers: a Core compare and membership test (subtotal >= threshold and a fast-lane priority) OR a Rich CEL guard (subtotal + tip >= 6000), so a generous order or a flagged big basket is admitted.

Admitted orders enter Active, an orthogonal superstate running two parallel regions at once:

  • Fulfillment is the work spine: Cooking supervises a kitchen actor, whose plated output advances to AwaitingCourier, then EnRoute supervises a courier actor.
  • Watchdog is an SLA clock. An after(30m) delayed transition fires SLABreached if the delivery window elapses, recording the breach in the order’s context without blocking delivery.

Each region’s actor messages the order on completion; an assign reducer, the order’s only context writer, folds the result in.

The courier’s DroppedOff is a cross-cutting transition on the Active compound: it exits the whole parallel configuration to Settling, which captures the held payment and runs to the Delivered terminal.

Cancellation is a saga. Cancel on Active exits to Refunding, which invokes a refund service to reverse the authorization hold, the compensating action, before reaching the Canceled terminal.

m, err := fooddelivery.NewModel()

Notably, NewModel forges the machine, then round-trips it through its IR (ToJSONLoadFromJSONProvide) so the CEL guard binds exactly as a host loading a serialized definition would: the in-repo proof of the serialization split. It can also snapshot and restore across a process restart mid-order.

This page shows the model. The companion examples/dispatch showcase runs the identical machine under the full runtime (durable, distributed, polyglot, and observed) so the same statechart that renders this diagram also survives restarts, fans out across processes, binds non-Go behavior, and emits telemetry. See integrating for the runtime surface.