The big picture
What you want to do
You want to see chapter 1's checkout written with Itinera, all at once, before learning any of it. Don't try to understand every line. The comments say what each piece is for, and answer chapter 1's questions where the code answers them. The chapters after this one teach each piece, as you need it.
The code
Start with the workflow. Looking at it, you can see at once what happens to every order: which steps run, in what order, how many times, what repairs a failure and who reacts, without reading any of the code it names.
/// The shop's workflow: reserve, charge and ship, auditing the charge, rescuing it when the
/// card has expired, and telling the customer how the order went.
#[workflow(name = "orders", policies = [Notify], reporters = [Log])]
// The steps run in this order, and nothing reorders them.
#[with_step(Reserve, name = "reserve")]
// The charge runs at most twice: once, and once more after its rescue step succeeds.
// Every successful charge is audited.
#[with_step(Charge, name = "charge", rescue = RenewCard, policies = [Audit])]
// Shipping may be tried once more, after a failure worth trying again.
#[with_step(Ship, name = "ship", retry_budget = 1)]
struct Orders;The business rules it lists are steps, one for each: reserve the goods, charge the card, renew an expired card, ship the parcel.
/// Reserves the order's goods in the stock store.
#[derive(Step)]
struct Reserve {
#[input]
order: String,
}
#[step]
impl Reserve {
fn run(self, #[service] stock: &dyn StockStore) -> Result<Outcome, Error> {
if !stock.reserve(&self.order) {
return Self::fail("out-of-stock");
}
Self::succeed()
}
}
/// Charges the card the price, less any discount. It only says how it went: what happens after
/// a declined card is not its business.
#[derive(Step)]
struct Charge {
#[input]
card: String,
#[input]
price: i64,
#[input]
discount: Option<i64>,
}
#[step]
impl Charge {
fn run(
self,
#[service] payments: &dyn Payments,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<Outcome, Error> {
let amount = self.price - self.discount.unwrap_or(0);
match payments.charge(&self.card, amount) {
Ok(receipt) => {
contributor.contribute("charged", amount);
contributor.contribute("receipt", receipt);
Self::succeed()
}
Err(Declined::ExpiredCard) => Self::fail("expired-card"),
Err(Declined::Refused) => Self::fail("declined"),
}
}
}
/// The charge's rescue step: asks the customer's bank for the card that replaced an expired one.
/// A charge that failed for any other reason, or ended in an abnormal termination, which has no
/// reason, cannot be repaired this way.
#[derive(RescueStep)]
struct RenewCard {
#[input]
card: String,
#[reason]
reason: Option<Reason>,
}
#[rescue_step]
impl RenewCard {
fn run(
self,
#[service] payments: &dyn Payments,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<RescueOutcome, Error> {
if self.reason.as_ref().map(Reason::code) != Some("expired-card") {
return Self::fail("nothing-to-renew");
}
let Some(card) = payments.renewed_card(&self.card) else {
return Self::fail("nothing-to-renew");
};
contributor.contribute("card", card);
Self::succeed()
}
}
/// Ships the order through the carrier it is given, without knowing which one it is. A carrier
/// that cannot take the parcel might be replaced on another try, so that failure is worth trying
/// again.
#[derive(Step)]
struct Ship {
#[input]
order: String,
#[input]
weight: i64,
}
#[step]
impl Ship {
fn run(
self,
#[service] carrier: &dyn Carrier,
#[service] stock: &dyn StockStore,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<Outcome, Error> {
let Some(tracking) = carrier.send(&self.order, self.weight) else {
return Self::fail("parcel-refused");
};
stock.hand_over(&self.order);
contributor.contribute("tracking", tracking);
Self::succeed()
}
}The policies it attaches react to how things went: audit a successful charge, tell the customer at the end.
/// Audits every successful charge, in the journey's events, including one after a rescue.
#[derive(Default)]
struct Audit;
#[step_policy(name = "audit")]
impl Audit {
#[on_step_success]
fn audited(
&self,
#[from_step] charged: i64,
#[from_step] receipt: String,
#[reporter] mut reporter: HookReporter<'_>,
) -> Result<Option<OnSuccess>, Error> {
reporter.info(format!("charged {charged}, receipt {receipt}"))?;
Self::carry_on()
}
}
/// A role the shop provides: telling a customer something.
trait Messages {
fn tell(&self, customer: &str, message: &str);
}
/// Tells the customer how the order went, once, when the journey ends.
#[derive(Default)]
struct Notify;
#[workflow_policy(name = "notify")]
impl Notify {
#[on_workflow_success]
fn shipped(
&self,
#[from_workflow] customer: String,
#[from_workflow] order: String,
#[from_workflow] tracking: String,
#[role] messages: &dyn Messages,
) -> Result<(), Error> {
messages.tell(
&customer,
&format!("order {order} is on its way: {tracking}"),
);
Ok(())
}
#[on_workflow_failure]
fn failed(
&self,
#[from_workflow] customer: String,
#[from_workflow] order: String,
#[role] messages: &dyn Messages,
) -> Result<(), Error> {
messages.tell(&customer, &format!("order {order} could not be completed"));
Ok(())
}
}
Its impl blocks supply what the steps use: the bank, the warehouse and a carrier for every attempt, and the way the shop tells a customer something.
#[workflow]
impl Orders {
/// One payment client for the journey, used by the charge and its rescue step.
#[provider(key = "payments")]
fn bank(&self) -> Result<Provided<dyn Payments>, Error> {
Ok(provide!(BANK))
}
/// One stock store for the journey, shared by reserve and ship.
#[provider(key = "stock")]
fn warehouse(&self) -> Result<Provided<dyn StockStore>, Error> {
Ok(provide!(WAREHOUSE))
}
/// A carrier for every attempt: the main carrier first, the backup carrier after.
#[provider(per_attempt)]
fn carrier(&self, #[attempt] attempt: NonZeroU32) -> Result<Provided<dyn Carrier>, Error> {
if attempt.get() == 1 {
Ok(provide!(MAIN_CARRIER))
} else {
Ok(provide!(BACKUP_CARRIER))
}
}
}
/// How the shop tells a customer something, for the notify policy: here, by printing it.
#[role]
impl Messages for Orders {
fn tell(&self, customer: &str, message: &str) {
println!(" to {customer}: {message}");
}
}The bank, the warehouse and the carriers are faked, as in chapter 1, so the program prints the same thing every time. Before it runs anything, it prints the workflow's listing:
// The workflow lists its steps, their policies, services and rescue step, without running
// anything.
for listed in Orders::descriptor().listing() {
println!("{}", entry(&listed));
}Then it runs seven orders. Ann, Ben, Cat and Dan come from chapter 1, with the same data. Three more customers take the paths none of them takes: Eve's goods are out of stock, Fin's card is valid but the order is above the bank's limit of 1,000, and Gil's parcel weighs 60 kg, too heavy for both carriers.
// The order, the customer, the card, the price, the discount and the parcel's weight in kg.
let orders = [
("A-1", "Ann", "ann-card-2028", 40, Some(5), 2),
("A-2", "Ben", "ben-card-2024", 25, None, 1),
("A-3", "Cat", "cat-card-2027", 30, None, 30),
("A-4", "Dan", "dan-card-2025", 1200, None, 3),
("A-5", "Eve", "eve-card-2027", 20, None, 2),
("A-6", "Fin", "fin-card-2028", 1500, None, 4),
("A-7", "Gil", "gil-card-2029", 45, None, 60),
];It prints the listing, then, under each order, what the shop's log and the customer were told, and how it ended:
1. reserve; services: stock from warehouse
2. charge, with audit; services: payments from bank; rescued by charge_rescue, services: payments from bank
3. ship; services: carrier from carrier, stock from warehouse
A-1:
INFO audit: charged 35, receipt R-ann-card-2028-35
to Ann: order A-1 is on its way: T-A-1
succeeded
A-2:
WARN charge, attempt 1: expired-card
INFO charge to be rescued by charge_rescue
INFO audit: charged 25, receipt R-ben-card-2029-25
to Ben: order A-2 is on its way: T-A-2
succeeded
A-3:
INFO audit: charged 30, receipt R-cat-card-2027-30
WARN ship, attempt 1: parcel-refused
INFO ship tried again
to Cat: order A-3 is on its way: B-A-3
succeeded
A-4:
WARN charge, attempt 1: expired-card
INFO charge to be rescued by charge_rescue
WARN charge, attempt 2: declined
WARN charge given up: failure
to Dan: order A-4 could not be completed
failed: failure, declined
A-5:
WARN reserve, attempt 1: out-of-stock
WARN reserve given up: failure
to Eve: order A-5 could not be completed
failed: failure, out-of-stock
A-6:
WARN charge, attempt 1: declined
INFO charge to be rescued by charge_rescue
WARN charge_rescue, attempt 1: nothing-to-renew
WARN charge_rescue given up: failure
to Fin: order A-6 could not be completed
failed: failure, nothing-to-renew
A-7:
INFO audit: charged 45, receipt R-gil-card-2029-45
WARN ship, attempt 1: parcel-refused
INFO ship tried again
WARN ship, attempt 2: parcel-refused
WARN ship given up: retries exhausted
to Gil: order A-7 could not be completed
failed: retries exhausted, parcel-refusedHow it works
Every order follows the path the diagram draws, and the output shows which.
flowchart TD accTitle: A journey through the shop accDescr: The journey reserves the goods, then charges the card. A successful charge is audited, then the order is shipped. A failed charge is rescued by charge_rescue; when the rescue succeeds the charge runs once more, and when the rescue or the second charge fails the journey fails. Shipping is tried with the main carrier; when it cannot take the parcel, shipping is tried once more with the backup carrier. When shipping succeeds, notify tells the customer and the journey succeeds. Any failure that is not repaired leads notify to tell the customer, and the journey fails. start([The journey starts with the order]) --> reserve[reserve] reserve -- success --> charge[charge] reserve -- failure --> told_failed[notify tells the customer the order could not be completed] charge -- success --> audit[audit logs the charge] charge -- failure --> rescue[charge_rescue asks the bank for the new card] rescue -- success --> again[charge runs once more] rescue -- failure --> told_failed again -- success --> audit again -- failure --> told_failed audit --> ship[ship, attempt 1, main carrier] ship -- success --> told_shipped[notify tells the customer the order is on its way] ship -- failure, retry budget left --> ship2[ship, attempt 2, backup carrier] ship2 -- success --> told_shipped ship2 -- failure --> told_failed told_shipped --> succeeded([The journey succeeded]) told_failed --> failed([The journey failed])
The diagram shows:
- The journey starts with the order's data and runs
reserve. If reserving fails, as Eve's does, the journey goes to its end as failed. - Then
chargeruns. When it succeeds, as Ann's does, theauditpolicy logs the charge. - When the charge fails, the rescue step, which the listing names
charge_rescue, runs and asks the bank for the new card. If the rescue fails, as Fin's does, the journey goes to its end as failed. - When the rescue succeeds, the charge runs once more. If it succeeds, as Ben's does,
auditlogs it; if it fails, as Dan's does, the journey goes to its end as failed. - After the audit,
shipmakes its first attempt, with the main carrier. If it succeeds, as Ann's does, the journey goes to its end as succeeded. - If the first attempt fails,
shiphas a retry left, so it makes a second attempt, with the backup carrier. If that succeeds, as Cat's does, the journey goes to its end as succeeded; if it fails, as Gil's does, the journey goes to its end as failed. - At a successful end,
notifytells the customer the order is on its way, and the journey succeeded. - At a failed end,
notifytells the customer the order could not be completed, and the journey failed.
Outcomes
We have shown the whole checkout as one workflow, where every question of chapter 1 is answered in one place.
| The usual way | With Itinera |
|---|---|
| Retries, repairs and fallbacks usually live in the functions that meet the failure, so finding them means reading those functions. | Every retry budget, rescue step and policy is in the workflow's declaration. |
| Which services a function uses is often known only from its body or its constructor. | The listing shows every step's services and where each comes from, before anything runs. |