Skip to content

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. move names 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 / : B parameter that the other role never receives). Only Bank A can evaluate amount <= 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 from B and the receipt from A — 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

  1. 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.
  2. 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.
  3. 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:

$ python examples_business.py
$ python -m pytest tests/test_business.py