Guide

Checkout touches two systems: the seat inventory and the payment provider. They cannot share one transaction, so the order of steps decides what happens when something fails in between. The rule is to capture money only after the seat is committed.

Concept

Before you start

  • Authorization: the card network reserves funds for a payment. Capture actually takes them. A void releases an authorization that will not be captured.
  • Saga: a multi-step process where each step has an undo action that runs if a later step fails. See distributed transactions.

The big idea

Picture buying a ticket at a counter. The clerk first confirms the seat is still yours, then runs your card. If the clerk ran the card first and then found the seat gone, you would need a refund, which takes days and erodes trust.

The picture breaks when the system crashes between steps. Then you need recovery, not just ordering.

Walking through it

The safe order is authorize, commit the seat, then capture. Here are the three cases:

Happy path:
  hold valid → authorize → UPDATE seats to booked → capture → ticket

Payment declined:
  hold valid → authorize declines → release hold (or let TTL expire)

Seat commit loses the race:
  authorize OK → UPDATE returns 0 rows → void authorization → buyer retries

In the last case, the buyer's card has a hold on it that is never captured. Voiding it releases the funds, so the buyer is not charged.

A crash is the harder case. If the system dies after authorization but before the seat commit, a reconciliation job either voids the authorization or completes the commit from a durable outbox. Payment calls carry an idempotency key, so retrying a timed-out request cannot create a second charge. See idempotency.

Common mistakes

  • Capturing before the seat commit. A buyer who loses the seat has been charged for nothing.
  • Forgetting to void after a lost race. Funds stay on hold on the buyer's card.
  • Retrying a payment without an idempotency key. A timeout followed by a retry can charge twice.
  • Treating a payment timeout as a failure. The outcome is unknown, so reconcile instead of guessing.

Check yourself

1. The seat commit returns 0 rows after authorization succeeded. What happens to the buyer's money?

Answer: The authorization is voided, which releases the funds, so the buyer is not charged.

2. Why not capture first and commit the seat afterwards?

Answer: If the commit then fails, you hold money for a seat you cannot give, and you must refund it.

Key takeaways

  • Checkout spans two systems, so it runs as a saga, not one transaction.
  • Authorize, commit the seat, then capture.
  • Every failure path needs an undo: release the hold or void the authorization.
  • Use idempotency keys and reconciliation to handle timeouts and crashes.

Further learning

Return to the ticket booking case study for the full design, or read the payment system case study for the payment side in depth.