Two people book the last seat at the same moment. Neither is allowed to see a failure after paying. What happens?
Preventing double booking seats means claiming the seat before payment, not racing two payments and reconciling later. A short atomic hold serialises buyers, payment only starts for the winner, and uncertain payment outcomes are settled by reconciliation rather than silent expiry.
5 min readUpdated 2026-08-15
0
Answer
Short answer
Create an atomic short-lived hold on the seat, send only the holder to payment, reject everyone else at selection time, and reconcile unknown payment outcomes before releasing the hold.
Was this answer helpful?
Likely follow-ups
→The user abandons the card form with a hold outstanding and returns eleven minutes later. What do they see, and what did the system do in between?
→Your inventory count is served from a read replica two seconds behind. Which user-visible promise does that break first?
→The business wants deliberate overbooking on some routes. What changes in the model, and what must not change?
→A single booking covers four seats and only three are available. Where does that partial failure surface, and what is the user offered?
The constraint is about ordering, not about locking
Read the second sentence of the question again. Nobody may see a failure after paying. That is not a database requirement, it is a statement about where in the sequence the bad news is allowed to appear.
Which means the design writes itself in one move: take the money after the seat is secured, never before. If payment comes first and inventory second, then the losing user has already been charged when you discover the seat is gone, and every recovery from that point is a refund and an apology. A refund is a visible failure. It arrives days later, it involves a support conversation, and the user tells other people about it.
So the refusal moves upstream. Both users click the last seat. Both attempt a claim. One gets it and proceeds to the card form. The other is told, before any payment interface appears, that the seat has just gone, and is shown the seat map with alternatives. That user has had a disappointing experience and not a failed one, and the distinction is the entire answer.
Say this plainly in an interview, because it separates the ordering insight from the mechanics: "A race for the last seat should be lost at selection, not at settlement."
The claim itself
The seat needs one authoritative record with serialised transitions, and the claim has to be a single operation that both checks and mutates. A conditional update that sets the seat to held with this owner and this expiry, only where the seat is currently free or where an existing hold has already lapsed, and you read the affected row count as the verdict. One row means you hold it. Zero means somebody else does.
The hold needs three things and candidates routinely supply only the first. A state, so the seat is not available. An owner, so a second request from the same user resumes their own hold instead of being refused. And an expiry, so a user who wanders off does not remove inventory from sale permanently.
Everything the user is shown flows from those states. Available seats are selectable. A seat you hold shows a countdown, because a hidden timer that lapses silently is the source of the worst version of this experience: a user completing a card form against a seat they no longer have. A seat held by someone else is drawn as unavailable, and refreshing does not get it back.
Loading diagram…
The line to look at is the one that never reaches the card form. Buyer B is refused while still on the seat map, which is the whole design goal, and everything after it concerns only the buyer who won.
The hold that must not expire
Now the hard case, which is where an answer stops being a textbook one. Buyer A submits payment. You call the provider. The connection times out and you do not know whether the charge succeeded.
Two obvious moves are both wrong. Releasing the seat because the hold has expired risks selling it again to someone else while a successful charge sits on Buyer A's card, which is the exact failure the question forbids. Retrying the authorisation blindly risks charging twice.
So the hold enters a state of its own: payment pending, owned by a specific payment attempt with its own identifier. Holds in that state are excluded from expiry-driven release. They are resolved by asking the provider about that attempt identifier until it gives a definite answer, and only then does the seat become sold or return to available. The distinction to articulate is between a hold nobody is using, which expiry should reclaim, and a hold whose fate is unknown, which only reconciliation can settle.
That also means the timer the user sees and the lifetime of the hold are not the same thing. The countdown expires the checkout session. The hold survives into reconciliation if a payment was attempted. Conflating those two produces the release-while-charging bug, and it is the single most common flaw in an otherwise sound answer.
What you do when it happens anyway
An interviewer may press: suppose the money moved and the seat is gone. What then?
Do not pretend the design prevents it. It reduces the probability; it does not reach zero, because a stale inventory read, an oversell during a failover, or a bug can all produce it. Once it has happened, the response is policy, not code. Refund in full and rebook at your cost on the next available option, upgrade if that is what is available, notify the user before they discover it themselves, and make the decision automatic up to some value so that the recovery does not wait on a human. Write that policy down and implement it as a path in the system, with a queue somebody watches.
Then measure it. The number of bookings that took money and could not be honoured is the metric this design exists to keep at zero, and it should be on a dashboard rather than inferred from complaints.
Deliberate overbooking is worth separating from all of this. Airlines sell more seats than they have on purpose, with a compensation budget and a legal framework behind it, and that is a business decision with a known cost. It is not the same thing as an accidental double-sell, and using it to excuse one is the answer an interviewer stops trusting.
The idea that has to survive the whole design
Two independent claims on a single unit of inventory need one serialisation point, and that point has to be the record itself. Not a cached count, not an application-level check, not an availability number computed from a replica. Those all produce the same failure in different clothes: two callers both believing the seat is free.
Everything else in the design is about making the consequence of losing that race land early and land gently.
Order of operations is the requirement: claim inventory, then take money, and a hold whose payment outcome is unknown belongs to reconciliation rather than to a timer.