Two people can open the same free slot. Both requests read “available.” Both insert. The court, room, or person is now double-sold. The bug is the gap between the read and the write. It is not a careless receptionist, and it is not something the UI can fix by hiding the button.
Courtside Alley booking lessons, booking and ops apps, and domain modeling all name this failure. None of them specify the lock. This article does. The venue story stays in those pieces.
The constraint is the lock
if (isAvailable()) { create(); } loses under concurrency, even inside a transaction, unless that transaction holds a lock or the insert cannot violate a rule.
When slots are fixed and cannot overlap by construction, a unique index on the resource and the start time is enough. When bookings have a duration, the rule is “these ranges must not overlap.” PostgreSQL exclusion constraints express that. PostgreSQL or MySQL is why we do not pretend every engine has the same tool.
SELECT … FOR UPDATE works when there is a row to lock, such as a pre-seeded slot. If “free” means the absence of a row, there is nothing to lock. The constraint has to reject the second insert.
Holds expire
A slot someone is paying for is not free and not yet confirmed. Write a hold with an expiry, and let the same constraint cover holds and confirmed bookings. When the hold expires, release the row. Do not keep the hold in the browser and hope the second customer waits.
The payment webhook that confirms the hold has to be safe to receive twice. That is the idempotency problem in one webhook, one side effect, applied to this row.
Straight next step
Tell us what is being reserved — a court, a room, a person, or a seat — and whether two bookings can overlap in time. We will name the constraint. We will not start from a calendar widget.



