Skip to content
Development Practice

High-Demand Bookings Without Overselling: Designing for Contention

12 min read

Overselling is nearly always the same bug: the capacity check and the booking write happen as two separate steps, so concurrent requests race between them. The fix is architectural. Make check-and-write one atomic action, serialise it per bookable item so the critical section is milliseconds long, make every operation idempotent, and give overflow demand a waitlist instead of an error page. This guide explains the failure modes and the design that prevents them, as implemented in the Booking Engine accelerator.

What actually goes wrong when demand spikes?

The classic failure is the last-place race. A session has one place left. Two customers tap “book” within the same second. Both requests read the availability, both see one place, both pass the check, and both write a booking. The session is now oversold, and no amount of retry logic after the fact can un-sell it.

Notice what did not cause this: load. The system might be running at two percent CPU. The race needs only two requests and bad sequencing, which is why overselling shows up at modest scale and gets steadily worse as a business grows. High demand does not create the bug; it buys more lottery tickets for it.

The other spike failures are secondary but real:

  • Phantom fullness: abandoned checkouts hold places that never get released, so sessions look sold out while revenue walks away.
  • Duplicate bookings: a customer double-taps, or a payment webhook retries, and the same booking lands twice.
  • Collapse under retry storms: the system slows, clients retry, and the retries become most of the traffic.

Each has a specific design answer, and they compound: a system that races will also duplicate, because racing systems rarely have idempotency either.

Why does “check availability, then book” always break?

Because between the check and the write, the world can change, and under contention it reliably does. This is a time-of-check to time-of-use race: the check result is stale the instant it is produced, and any decision based on it is a guess.

It cannot be fixed where people usually try to fix it:

  • Not in the front end: disabling the button after one tap helps politeness, not correctness. Two different customers still race.
  • Not with faster reads: a “real-time” availability cache narrows the window; it cannot close it. Two requests can still read the same value.
  • Not with more application servers: scaling out makes it worse. Servers cannot see each other’s in-flight requests, so the race gets more lanes.

The only fix is to remove the gap: the capacity check and the booking write must be one atomic action at the layer that owns the data. Everything else in high-demand design is arranged around keeping that atomic action small and fast.

How should the atomic booking action be built?

Serialise per bookable item, and keep the critical section tiny. The pattern that works in SQL-backed systems is a per-item mutex: the transaction takes an update lock on the specific session occurrence row, checks capacity, writes the hold or booking, updates the counters, and commits. Concurrent requests for the same occurrence queue behind the lock for a few milliseconds. Requests for different occurrences do not interact at all.

This is pessimistic concurrency, and for bookings it is usually the right choice. The alternative, optimistic concurrency, lets every request proceed and detects collisions at write time via a row version, retrying the losers. Optimistic wins when conflicts are rare. On a hot occurrence during a spike, conflicts are the norm, so optimistic designs burn their throughput on retries and finish in arrival order anyway, minus fairness.

Whichever strategy sits at the core, the surrounding rules are the same:

  • Scope the lock to one item: a lock per occurrence serialises the one race that matters. A table-level or service-level lock would serialise the whole business.
  • Do nothing slow inside the lock: no payment calls, no emails, no external APIs. Check, write, commit. Payment happens against a time-boxed reservation hold, outside the critical section.
  • Let the database enforce the invariants: uniqueness constraints (one active booking per customer per occurrence) catch anything the application logic misses.

This is how the Booking Engine accelerator implements its public reserve endpoint: the occurrence row acts as a per-occurrence mutex, the capacity check counts confirmed bookings plus unexpired holds, and the hold is written in the same serialised transaction. The proc’s contract is explicit that concurrent callers are serialised rather than raced.

Where does idempotency fit in?

Everywhere a request can arrive twice, which under load is everywhere. Customers double-tap. Mobile networks time out and resend. Payment providers deliver webhooks at least once, not exactly once. A booking system that treats every arrival as new will duplicate bookings precisely when it is busiest.

The rule: every mutating operation needs a natural key that makes repeats safe.

  • Reserve: keyed by customer and occurrence. A second reserve call returns the existing hold rather than creating a rival one.
  • Confirm: keyed by the reservation. A replayed confirmation returns “already confirmed” instead of incrementing the booked count twice.
  • Cancel: cancelling an already-cancelled booking is a no-op success.

Idempotency also quietly defuses retry storms. If retries are safe, clients can retry aggressively without corrupting state, and the system can shed load with clean errors knowing the caller will simply try again.

What should happen to demand that exceeds capacity?

Give it somewhere to go. When a spike fills a session in ninety seconds, the remaining demand is not noise, it is a ranked list of customers who wanted to pay you. An error page throws that list away; a waitlist keeps it.

The waitlist join should live inside the same atomic action as the reserve. When the capacity check fails, the same transaction records the customer at the back of the queue and returns their position. There is no second race between “booking failed” and “waitlist joined”, and the customer gets a definite answer, not a shrug. This is the behaviour of the accelerator’s reserve endpoint: full sessions return a waitlist outcome with a queue position, atomically.

Two further pressure valves are worth designing in:

  • A deliberate overbooking allowance: an explicit per-session number of places accepted beyond capacity, set by staff to absorb predictable no-shows. Deliberate overbooking is a policy; accidental overbooking is a defect. The data model should support the first and prevent the second.
  • A queue in front, at extreme scale: when demand outruns capacity by orders of magnitude (a ticket on-sale, a term-start registration rush), put a waiting room ahead of checkout so the booking core sees orderly traffic. The atomic core does not change; it just receives requests at a survivable rate.

For the full mechanics of queue design, positions, and what should happen when places free up, see how booking waitlists should work.

How do you evaluate a system’s contention story?

Ask for demonstrations, not adjectives. “Real-time availability” and “handles high traffic” are marketing phrases; contention safety is a set of specific, testable behaviours.

  • The last-place test: fire two simultaneous bookings at a session with one place. Exactly one should succeed, and the other should get a waitlist offer, every time.
  • The replay test: send the same confirmation twice. The booked count must increment once.
  • The abandonment test: reserve, walk away, and watch whether the place frees itself at the hold boundary without a manual fix.
  • The blast-radius test: hammer one occurrence and measure whether bookings for other occurrences slow down. Item-scoped serialisation should keep them independent.
  • The invariant question: ask where the final capacity guarantee is enforced. The answer should name the database transaction, not the front end.

A vendor with a real design will enjoy these questions. Structure the wider evaluation with our franchise software buyer’s guide, which applies the same demonstrate-not-describe discipline to every capability.

Where does this fit in a wider platform?

Contention safety is a property of the whole flow, not a feature bolted onto one endpoint. Capacity modelling, reservation holds, waitlists, and payment orchestration all have to respect the same invariants, which is why retrofitting it onto a system that races is expensive.

The Booking Engine accelerator was built with these patterns from the start: serialised atomic reserve and confirm, time-boxed holds, positioned waitlists, per-occurrence capacity counters, and an explicit overbooked-capacity field for deliberate policy. It ships as a perpetual source-code licence, so the concurrency design is yours to inspect. Where requirements go beyond any packaged platform, our custom software development service applies the same engineering discipline to a bespoke build.

Book a contention review

If your booking system has ever oversold, duplicated a booking, or slowed to a crawl on a busy morning, the cause is usually one of the races described above. Bring your flow and we will map where the checks and writes actually happen, and what closing the gaps would involve.

No pitch, no obligation. If your system is already sound, we will tell you.

Book a consultation

Frequently asked questions

Why do booking systems oversell under high demand?
Almost always because the capacity check and the booking write are separate steps. Two requests read 'one place left' at the same moment, both pass the check, and both write a booking. The fix is to make check-and-write a single atomic action, serialised per bookable item, so concurrent requests queue for milliseconds instead of racing.
What is the difference between optimistic and pessimistic concurrency for bookings?
Pessimistic concurrency takes a lock on the bookable item first, so only one request at a time can check capacity and write; others wait briefly. Optimistic concurrency lets requests proceed and detects conflicts at write time (for example via a row version), retrying the losers. For hot items with a hard capacity, short pessimistic serialisation is usually simpler and fairer.
How should a booking system handle an on-sale spike?
Keep the critical section tiny (one row lock, one capacity check, one insert), push everything else outside it, and give overflow demand a waitlist rather than an error. If demand routinely exceeds capacity by orders of magnitude, add a queue or waiting room in front so the booking core sees orderly traffic.
Does double-booking prevention belong in the application or the database?
The final guarantee belongs in the database, where the capacity data lives, enforced by an atomic transaction and uniqueness constraints. Application-level checks improve the experience but cannot be the only defence, because two application servers cannot see each other's in-flight requests.
What is an overbooking allowance and when is it deliberate?
An overbooking allowance is an explicit, configured number of places a venue will accept beyond stated capacity, usually to absorb predictable no-shows. It is deliberate when it is a field staff set per session with eyes open. It is a defect when it happens because the system raced. A good platform supports the first and makes the second impossible.

Ready to transform your software?

Let's talk about your project. Contact us for a free consultation and see how we can deliver a business-critical solution at startup speed.