A checkout is a distributed system, whether you designed one or not
By Panagiotis Athanasakopoulos · Παναγιώτης Αθανασακόπουλος
Architecture14 September 20263 min
The most expensive assumption in web development is that a checkout is a form. It looks like one: a few fields, a button, a thank-you page. But the moment a customer presses pay, at least three systems have to agree about what just happened — the payment provider that moved the money, the system that holds the stock or the room, and your own database, which is supposed to remember both. Those three do not share a transaction. They share a hope.
At TRVL, the online travel agency I run, a single booking crosses exactly that line: a payment provider, the hotel's own reservation system and our database. Every decision in that checkout starts from one assumption — that any of the three can be late, can say yes twice, or can say nothing at all. Design for the happy path and the happy path is the only one you will have handled.
There are three shapes of disagreement, and every checkout meets all of them eventually. The payment succeeds but your server never hears about it, because the notification was delayed, retried or lost. The notification arrives twice, and the second copy is processed as if it were new. Or the customer does something entirely reasonable — changes payment method, applies a discount code, goes back a step — and each of those quietly creates a new payment attempt alongside the old one.
The first defence is idempotency: every operation that moves money or stock must be safe to run twice. That means a key that identifies the operation rather than the request, and it means the database enforces it. A unique index on the payment reference is worth more than any amount of careful application code, because it is the only lock that survives a restart, a second server and a developer who did not read the comment.
The second is to decide, once, what counts as paid. It is not the browser coming back to your thank-you page. Tabs close, phones lose signal on a train, and a redirect is a courtesy rather than a record. The payment provider's own event — delivered to your server, verified, and acknowledged only after you have written it down — is the truth. The page the customer sees should read that truth, not create it.
The third is the one that catches experienced teams: attempts are not orders. When one checkout produces several payment attempts, the order has to follow the attempt that succeeded, not the first one that was created. Link the booking to the first attempt, let the customer pay on the third, and you have built the classic failure — a customer with a receipt and a booking your system believes never happened.
Which is why a checkout needs a reconciler as much as it needs a form. Something on a schedule has to compare what the payment provider says was paid with what your database says was fulfilled, and anything paid but unfulfilled goes to a human, loudly. Just as important, anything that tidies up abandoned orders must ask the provider before it marks one as failed. A cleanup job that trusts only your own database will, sooner or later, cancel an order somebody paid for.
Then there is partial success, the case most checkouts pretend does not exist. A customer books three rooms and the hotel confirms two. The wrong response is to retry the whole request, because the two that succeeded are real and a second attempt books them twice. The right one is to record exactly what you got, stop, and hand the remainder to a person — to complete by hand or to refund. A system that cannot represent 'two of three' will represent it as either three or none, and both are lies.
None of this is exotic. It is the ordinary discipline of distributed systems — idempotency, a single source of truth, reconciliation, compensation — applied to the one place every business actually has a distributed system, whether or not anyone drew it on a whiteboard. The form is the easy part. What happens in the ten seconds after the button is the product.
Common questions
- Why can't a checkout be a single database transaction?
- Because the money, the inventory and the order live in different systems. The payment provider, the reservation or warehouse system and the shop's own database each commit independently, so there is no transaction that spans all three. The checkout has to be designed to reach agreement between them after the fact — through idempotent operations, a single agreed source of truth for payment, and reconciliation.
- What is an idempotency key in payments?
- A value that identifies one logical operation — one payment, one booking — so that repeating the operation has no further effect. If a request or a notification arrives twice, the second is recognised by its key and ignored. It is strongest when the database itself enforces it with a unique constraint, rather than relying on application code to check first.
- Should an order be confirmed from the redirect or from the payment webhook?
- From the payment provider's server-to-server event, verified and recorded. The browser redirect can fail to arrive for ordinary reasons — a closed tab, a lost connection — so treating it as proof of payment produces paid customers with no order, and trusting it without verification lets anyone claim a payment they did not make.