The officiant asks each person, “Do you?” Nobody is married yet. Only once both have said “I do” does the officiant say “I now pronounce you married.” If either says no, nobody gets married.
Three systems are involved: inventory, payments and the courier. A coordinator runs the process.
Phase 1: Prepare. The coordinator asks each system: “Can you do your part? Get ready, but don’t finish yet.”
- Inventory sets the widget aside and locks it: “Ready.”
- Payments puts a hold on the card and locks it: “Ready.”
- Courier books a slot provisionally and locks it: “Ready.”
Phase 2: Commit.
- Everyone said ready: the coordinator says “Commit!” and each system finalizes its part.
- Anyone said no (say, no courier): the coordinator says “Abort!” and each system drops its hold. Nothing ever actually happened.
So there’s nothing to undo afterwards, because nothing is final until everyone agrees.
Code
"""A two-phase commit, in memory, for one order.
Three participants (inventory, payments, courier) and one coordinator.
Nothing is final until every participant has voted yes.
"""
class Participant:
"""One system taking part in the transaction."""
def __init__(self, name: str, can_do_it: bool = True):
self.name = name
self.can_do_it = can_do_it
self.state = "idle" # idle -> prepared -> committed | aborted
# ---- Phase 1 ----
def prepare(self, order_id: str) -> bool:
"""Get ready and lock what you need. Do NOT make it final yet."""
if not self.can_do_it:
print(f" {self.name}: NO, I can't do {order_id}")
return False
self.state = "prepared" # e.g. hold the stock, put a hold on the card
print(f" {self.name}: YES, prepared and locked for {order_id}")
return True
# ---- Phase 2 ----
def commit(self, order_id: str) -> None:
"""Make it final. Only called if EVERYONE said yes."""
self.state = "committed"
print(f" {self.name}: committed {order_id}")
def abort(self, order_id: str) -> None:
"""Throw away the preparation and release the locks."""
if self.state == "prepared":
print(f" {self.name}: aborted, released locks for {order_id}")
self.state = "aborted"
class Coordinator:
"""Asks everyone first, then tells everyone the same decision."""
def __init__(self, participants: list[Participant]):
self.participants = participants
def run(self, order_id: str) -> bool:
print("Phase 1: prepare (everyone votes)")
prepared = []
for p in self.participants:
if p.prepare(order_id):
prepared.append(p)
else:
# One NO is enough: stop asking and abort everyone who said yes.
print("Phase 2: ABORT")
for q in prepared:
q.abort(order_id)
return False
print("Phase 2: COMMIT (everyone said yes)")
for p in self.participants:
p.commit(order_id)
return True
if __name__ == "__main__":
print("=== Happy path ===")
Coordinator([
Participant("inventory"),
Participant("payments"),
Participant("courier"),
]).run("ORD-1")
print("\n=== The courier says no ===")
Coordinator([
Participant("inventory"),
Participant("payments"),
Participant("courier", can_do_it=False),
]).run("ORD-2")
So why not always use it?
- Everyone waits on everyone. Between the two phases, the widget and the card hold stay locked. If the courier takes 30 seconds to answer, other orders can’t touch those things for 30 seconds. At scale, that locking becomes a bottleneck.
- The coordinator is a single point of failure. If it crashes after “prepare” but before “commit”, every system sits there holding its locks with no idea whether to commit or abort.
- Every system has to take part. Stripe won’t hold a prepared transaction open while your courier makes up its mind. Most external APIs only offer “do it now”, with no “get ready and wait for my signal”.
Saga Pattern
| Two-phase commit | Saga | |
| When does it happen? | Only after everyone agrees | Each step, immediately |
| On failure | Nothing happened, just drop the holds | Undo what happened, last in first out |
| Locks | Held while everyone decides | None |
| Can others see partial work? | No | Yes |
| Works with any API? | No, every system must support “prepare” | Yes, as long as there’s an undo |