Business scenarios¶
The algorithms in Algorithms are the theory; these are the
problems a CTO recognizes from the office. Each one is a real, runnable
@choreography in examples_business.py, pinned by test, and deployable over
TCP with the same code you develop against locally with the simulator.
1. Two-bank wire transfer¶
The object lesson in distributed correctness: debit account A, credit account B, and tell both sides what happened — with the funds check handled at the bank that can see its own balance.
@choreography
def wire_transfer(amount, balance_a: A, balance_b: B):
can_pay = A(amount <= balance_a) # Bank A decides locally
if can_pay:
a_after = A(balance_a - amount) # Bank A debits
wire = move(amount, A, B) # the money moves A -> B
b_after = B(balance_b + wire) # Bank B credits
receipt = B(f"credited {wire}; new balance {b_after}")
rcv = move(receipt, B, A) # acknowledgment home
out = pack(a_after, b_after, rcv)
else:
note = A(f"insufficient funds: need {amount}, have {balance_a}")
out = pack(balance_a, balance_b, note)
return out
$ python examples_business.py
transfer ok: (60, 90, 'credited 40; new balance 90')
transfer rejected: (100, 50, 'insufficient funds: need 200, have 100')
What a CTO should notice:
- The money always arrives or the sender is told.
movenames a concrete source and destination; there is no "send and hope", no message that nobody receives, no ack that silently vanishes. The acknowledgment is part of the choreography. - The branch is decided by the role that can see it. Every bank keeps its
own balance (a
: A/: Bparameter that the other role never receives). Only Bank A can evaluateamount <= balance_a— and the projection enforces exactly that. - The return contract is checked, not hoped for. Both branches return
(balance at A, balance at B, message). An earlier version of this example returned the refusal message fromBand the receipt fromA— the type checker rejected it ("different locations on different branches") before the code ever ran. That is a bad-versioning-not-possible property.
2. Order fulfilment across four services¶
An order crosses order → payment → warehouse → shipping, and the audit trail comes back to the customer. The whole cross-service workflow is one function.
@choreography
def order_flow(item: A, qty: A) -> A:
ev_a = A(f"order {qty} x {item}") # Order service books it
inv = move(ev_a, A, B) # -> Payment
paid = B(f"paid {inv}") # Payment authorizes
wm = move(paid, B, C) # -> Warehouse
picked = C(f"picked {wm}") # Warehouse picks
shp = move(picked, C, D) # -> Shipping
track = D(f"tracking {shp}") # Shipping issues a tracking no.
done = move(track, D, A) # tracking no. back to the customer
trail = pack(ev_a, paid, picked, done)
return trail
$ python examples_business.py
order flow: ('order 3 x widget', 'paid order 3 x widget',
'picked paid order 3 x widget', 'tracking picked paid order 3 x widget')
The workflow lives in one place — no four service implementations to keep in sync, no integration contract drifting on one side. You can see the whole path (including the return edge) on one screen, and adding a step (e.g. an anti-fraud check between payment and warehouse) is one line of choreography, not a cross-team coordination ticket.
3. Credit-check request/reply¶
The most common distributed pattern there is: ask a downstream service, get a reply, branch on it.
@choreography
def credit_check(customer: A) -> A:
req = move(customer, A, B) # ask the bureau
ok = B(req["score"] >= 600) # the bureau decides
decided = move(ok, B, A) # reply home
if decided:
msg = A("approved")
else:
msg = A("declined")
return msg
$ python examples_business.py
credit ok: {'A': 'approved', 'B': NOOP}
credit declined: {'A': 'declined', 'B': NOOP}
Notice what cannot happen: a timeout with no answer, a reply decoded as opaque bytes by the caller, a "maybe" state. The reply is a typed value that returns to the caller, and the caller's branching is knowledge-of-choice — the caller decides because the reply lives there.
The cross-cutting story for a CTO¶
- The same code everywhere. The choreography you run in the simulator
(in-process,
< 1 s feedback) is byte-for-byte what you deploy over TCP (examples_network.py` runs two roles in two OS processes). Development, integration tests, and production speak one protocol. - Correctness is structural, not tested-in. There are no message layouts to get wrong, no partial states, no "reply lost" edge cases. Hand-written code needs weeks of fault-injection to approach this; here it falls out of projection and the location/type checker.
- Honest limits (see Why): a choreography assumes roles execute the interaction — it is not a crash/Byzantine model, and non-determinism must be explicitly lifted inside a role's computation. For the deterministic workflows above, that assumption is precisely what makes them analyzable.
Run them yourself: