Orders
A pet-store style order graph: rich events carry payloads, event[T] matches by type, and
timeouts either cancel payment or loop in shipping while ops is alerted.
Teaching slice of examples/.../petstore. Machine used below:
enum OrderState derives Finite:
case Created, PaymentProcessing, Paid, ShippingRequested, Shipped, Cancelled
enum OrderEvent derives Finite:
case InitiatePayment(orderId: Int, amount: BigDecimal)
case PaymentSucceeded(orderId: Int, txnId: String)
case PaymentFailed(orderId: Int, reason: String)
case RequestShipping(orderId: Int)
case ShipmentDispatched(orderId: Int, trackingId: String)
case PaymentTimeout, ShippingTimeout
val machine = Machine(
assembly[OrderState, OrderEvent](
(Created via event[InitiatePayment] to PaymentProcessing)
.onEntry { (e, _) => /* log */ ZIO.unit }
.producing { (e, _) =>
e match
case InitiatePayment(id, _) => ZIO.succeed(PaymentSucceeded(id, "txn-doc"))
case _ => ZIO.succeed(PaymentFailed(0, "unexpected"))
} @@ Aspect.timeout(5.minutes, PaymentTimeout),
PaymentProcessing via event[PaymentSucceeded] to Paid,
PaymentProcessing via event[PaymentFailed] to Cancelled,
PaymentProcessing via PaymentTimeout to Cancelled,
(Paid via event[RequestShipping] to ShippingRequested)
@@ Aspect.timeout(1.hour, ShippingTimeout),
ShippingRequested via event[ShipmentDispatched] to Shipped,
(ShippingRequested via ShippingTimeout to ShippingRequested)
.onEntry { (_, _) => ZIO.logWarning("shipping timeout - escalate to ops") },
)
)
The graph
Mermoid.diagram(
MermaidVisualizer.flowchart(machine),
DocsDiagrams.diagramConfig,
)Payment edges use event[InitiatePayment] / event[PaymentSucceeded] so runtime values carry
ids and amounts while the assembly stays declarative.
Pay then ship
InitiatePayment enters PaymentProcessing, the producing effect emits PaymentSucceeded,
then shipping completes:
ZIO.scoped {
for
fsm <- machine.start(Created)
_ <- fsm.send(InitiatePayment(1, BigDecimal(19.99)))
_ <- ZIO.sleep(50.millis)
_ <- fsm.send(RequestShipping(1))
_ <- fsm.send(ShipmentDispatched(1, "trk-1"))
state <- fsm.currentState
yield state
}.asDocShippedShipping timeout stays put
An explicit self-transition (to ShippingRequested) keeps the FSM put while the entry effect
can escalate. The full petstore example writes the same edge as to stay.
ZIO.scoped {
for
fsm <- machine.start(Created)
_ <- fsm.send(InitiatePayment(2, BigDecimal(5)))
_ <- ZIO.sleep(50.millis)
_ <- fsm.send(RequestShipping(2))
outcome <- fsm.send(ShippingTimeout)
state <- fsm.currentState
yield (outcome.result, state)
}.asDoc(Goto(ShippingRequested),ShippingRequested)