There are ten places in a class. Nine are booked. Two people see the final place and press Book.
Both requests can be perfectly reasonable. The application still has to produce only one additional booking.
This is a concurrency problem: actions overlap before the system has finished recording their effects. A useful booking system protects capacity at the point where it is committed, then gives each customer a clear account of the result.
The ten-seat class here is an illustrative example. The same principle applies to limited appointments, event places and other shared inventory.
Why the obvious check is not enough
A common mistake is to check availability and create the booking as separate, unprotected steps. Two requests can both read “one remaining” before either writes its reservation.
Disabling the button after a click helps the interface. It cannot coordinate two customers, two browser tabs or two application servers. The chair has become popular beyond the jurisdiction of the button.

Make the capacity decision atomic
The reservation operation needs to check and claim capacity through a suitable database boundary. Other requests must not be able to make an incompatible claim at the same time.
One approach updates a shared capacity record conditionally within a transaction and creates the reservation as part of that same unit of work. Another uses appropriate locking around the record representing the class. Optimistic concurrency can detect a conflicting change and require the application to reassess availability.
When off-the-shelf stops fitting, see how we approach custom development.
The important detail is what is protected. A concurrency token on each newly inserted booking does not automatically coordinate separate bookings competing for the same remaining place.
All paths that consume capacity need to follow the rule, including staff-created reservations. An override should be explicit if the business permits one; it should not emerge accidentally through a different screen.
Keep the database transaction short. Waiting for a person to enter payment details while holding a database lock would give everyone else a rather intimate relationship with their typing speed.
Hold a place while payment happens
If payment or another external step is required, the system may use a temporary reservation. That hold needs an expiry, a clear identity and a defined route to confirmation or release.
The business must decide what happens if payment succeeds after the hold expires. The system cannot simply confirm a place already allocated elsewhere. It needs to establish the current state and follow an agreed resolution process.
Repeated requests also need recognition. A retry of the same reservation attempt should return its existing outcome rather than consume another place. Idempotency protects that intended operation; capacity control protects the shared limit. The workflow needs both where repeats and competition are possible.
Check out this blog: Two customers. One last seat. Your database gets the casting vote.
Tell each customer what actually happened
The customer experience then needs to reflect the actual result. One person receives confirmation. The other receives a clear explanation that the last place is no longer available, with a suitable next option if the business offers one.
If the result is uncertain, explain that the system is checking before inviting another attempt. Keep entered information where appropriate so the person does not have to start from scratch.
Test the overlap directly. Send competing requests for the remaining place, retry a completed request and exercise hold expiry near confirmation. Inspect the records and external outcomes, not only the messages shown on screen.
A limited-capacity service makes a straightforward promise. The interface can be welcoming and the workflow can be flexible. The final count should remain wonderfully literal: ten seats means ten seats.