Two guests, one seat, and the race condition that costs you both

Accidental overbooking comes from race conditions, stale channels and weak cancellation paths. The target is zero double bookings nobody chose.

Every booking system has the same critical moment. Two people want the same thing at the same time.

It might be one seat, one room, one table, or one slot. In a demo, this moment usually looks safe, because requests arrive politely, one at a time. Real systems do not behave that way. Real guests click at the same time, agencies retry, payments lag, and channels update on their own schedules.

What happens in that moment tells you the quality of the system. If the system gets it wrong, you have sold something twice.

The refund is only the visible cost. The real cost is a guest standing at a desk at eleven at night, being told there is no room. It is also the member of staff who has to deliver that news without having done anything wrong.

Availability must never fall below zero by accident

We start these engagements by writing down one sentence and getting the client to agree to it: available inventory may never go below zero as a result of a race between requests.

If the business wants to overbook, that choice belongs in policy. It needs a number attached, deliberately configured, visible on a screen, and owned by a revenue manager. It should never appear by accident because two requests arrived together.

That distinction can sound pedantic, and it is the whole thing. Deliberate overbooking is a well understood commercial technique. Compensation is budgeted, and staff are trained. Accidental overbooking is an outage that happens to one guest at a time. It stays invisible in aggregate metrics, and it teaches your front desk that the system lies.

The simple version fails when two requests arrive together

The obvious implementation reads the availability, checks it is greater than zero, and writes a booking.

The problem sits between the read and the write. There is a window there, and at any meaningful volume that window will be occupied by somebody else doing the same thing. Two requests both read one remaining. Both conclude they may proceed. Both write.

The failure rate is low, which makes the problem hard to spot. At ten bookings a minute it might happen once a month, which looks like a mystery rather than a pattern. When you are at a hundred a minute during a sale, it becomes a daily event. By then it is buried in a system nobody wants to touch.

The usual fixes trade speed, certainty and clarity

Optimistic concurrency is the usual answer, and it is a reasonable one. The row carries a version. The update asserts the version has not changed. The loser is told to retry.

That approach works, it is cheap, and it fails cleanly rather than silently. That clean failure is the main thing. The system knows that one request lost, instead of quietly selling the same unit twice.

Pessimistic locking is stronger and slower. You take a lock on the inventory row for the duration of the transaction. That serialises everyone wanting that room type on that date. The result is correct, and it becomes your bottleneck during exactly the ten minutes you most wanted throughput.

The approach we usually end up with is a reservation model. Availability is decremented by a short-lived hold taken at the start of checkout and released automatically if the booking is completed without becoming final. That hold gives the guest a guarantee while they enter their card details. It also bounds the contention window to something predictable.

Most important, it makes the expiry policy an explicit business decision. The alternative is letting that policy appear as an accident of timeout configuration.

The refused guest needs the same care as the successful guest

Most design attention goes to the guest who succeeds. The guest who is refused is the one who forms an opinion about your brand.

There is an enormous difference between a clean, immediate message and a failure after delay. A clean response says, "that room was taken while you were deciding, here are three alternatives at the same rate". A poor response gives the guest a spinner followed by an error. Worse, it gives a confirmation followed by an email two hours later cancelling it.

We treat the refusal path as a first-class journey with its own design, its own copy and its own tests. That path runs most often on the days that matter commercially. It is almost always the path nobody has looked at.

Dates, rates, policies and money belong together

The second recurring failure in this sector is modelling payment separately from the stay. At first, that seems reasonable. A booking has dates. A payment has an amount. They relate to each other by an identifier.

Then somebody changes a date.

At that point, the questions multiply:

  • Was the rate for the new dates different.
  • Is the cancellation policy for the new dates different.
  • If the guest is now inside a penalty window that they were not in before, who absorbed that.
  • If the stay is shortened, is the refund calculated on the original rate or the current one.
  • If it is a partial refund on a payment that was captured in a different currency, at which exchange rate.

Systems that model these as separate concerns end up with the answers scattered across application code, support macros and a spreadsheet somebody maintains.

We model the booking as a single object whose state includes dates, rate, policy and financial position together. Every change is a transition that recalculates all of them at once and records why. Refund disputes essentially disappear, because the system can always show its working.

Channels add delay, and you still have to sell through them

A property is rarely selling through one channel. The usual set includes several routes to market:

  • The direct site.
  • Two or three online agencies.
  • A global distribution system.
  • Possibly a wholesaler.

Each channel holds its own idea of availability. Each updates on its own schedule. The agency will happily sell a room it believes exists.

You cannot achieve perfect consistency across all of them, and pretending otherwise leads to bad architecture. What you can achieve is a single authoritative source, aggressive push updates rather than polled pulls, and a deliberate buffer on channels with slower propagation.

The buffer is a commercial decision. It should be visible and adjustable, instead of being a constant somebody hard-coded in 2019.

Staff need to see channel failures before guests do

When a channel connection drops, the property must know before a guest arrives at a desk to find out. This sounds obvious, and it is routinely absent. The reason is that integration health is treated as an IT concern rather than an operational one.

The screen that matters is a front desk screen, not a technical dashboard. It should say, in plain language, that bookings from a named channel have not been received since a particular time.

That single feature has prevented more bad nights than any amount of retry logic. It lets a human make a decision with the information they need.

Slow search costs bookings

Search response time in travel maps to conversion with unusual directness. A search that takes four seconds instead of one costs a measurable proportion of bookings. The effect compounds because travel shoppers compare across several sites in parallel.

Search is also the hardest thing to make fast. It has to consider several things at once:

  • Availability across dates.
  • Rates.
  • Restrictions.
  • Promotions.
  • Channel rules.

The honest answer is usually caching with a clear staleness budget, rather than computing everything live. Caching means keeping a recent answer ready so the system can respond quickly. The staleness budget is the business decision about how out of date that answer may be, and for how long.

We treat the acceptable staleness as a business decision: how wrong is a price allowed to be for how long, and what happens at the point of booking when the cached price and the live price disagree. That last question has to be answered explicitly. The alternative is a guest seeing one number and being charged another.

Repeated confirm clicks must return the same result

Guests on hotel wifi, on phones, at the end of a long day, will tap the confirm button more than once.

The same discipline that protects a retail checkout protects a booking. A key generated by the client is honoured by the server, and the server returns the original outcome rather than creating a second reservation. This is idempotency, the rule that the same request repeated should not create a new result each time.

Without it, the double booking is a guest double booked against themselves, instead of a race between two guests. That is somehow more annoying to unwind because both records look entirely legitimate.

Group bookings need their own model

Everything above assumes a booking is one unit. Then a coach party wants eleven rooms, a wedding wants the whole floor on a Saturday in June, and a conference wants a block held for four months against a contract signed by somebody in sales.

Group inventory does not behave like transient inventory. Modelling it as a loop over single bookings produces bad outcomes in both directions.

The bad outcomes are familiar:

  • Held blocks that never convert quietly strangle availability on the best dates of the year.
  • Blocks released too aggressively leave a corporate client without the rooms they were promised.
  • A partial group booking, where nine of eleven rooms are available, is a commercial decision rather than a failure, and somebody has to be asked.

The pattern that works is to treat a block as its own object with an owner, a release schedule and a conversion deadline. That object sits between the property and the transient inventory pool.

It reserves against the same authoritative source, so the invariant still holds. Its lifecycle is managed by a human on a timescale of weeks, rather than by a timeout on a timescale of minutes.

Systems that skip this end up with revenue managers keeping the real block picture in a spreadsheet. That is how you get a room sold twice by two people who were both looking at accurate screens.

Cancellations carry the same risk as bookings

Booking engines get demonstrated forwards. Somebody searches, selects, pays, and sees a confirmation. Almost nobody demonstrates the reverse. The reverse is where the operational cost and most of the complaints actually sit.

A cancellation touches several parts of the system:

  • Inventory.
  • Payment.
  • Policy.
  • Channel propagation.
  • Often a loyalty balance.

It has to do so correctly when it arrives at two in the morning through an agency API, rather than through your own interface. It has to be idempotent, because agencies retry. It has to be correct when the stay has partially occurred, which is a surprisingly common case for multi-night bookings.

Releasing the inventory has to be atomic with recording the refund position. Atomic means the system records the whole change together. Otherwise, you will occasionally release a room and forget the money, or take the money and hold the room.

We build and test the cancellation path with the same care as the booking path. We also treat the time between a cancellation being received and the room being sellable again as a metric.

At a busy property in high season, an hour of delay there is a real room-night lost. Nobody was ever going to notice it in a dashboard of successful bookings.

We test the paths that fail under pressure

Before go live, we test the cases that prove the system behaves correctly when requests collide, channels fail, or prices change.

  • Concurrent requests for the last unit, at volume, asserting availability never goes negative.
  • Hold expiry under load, including the case where a payment succeeds after a hold has expired.
  • A channel connection dropping mid-day, and what the front desk sees.
  • A date change that crosses a cancellation policy boundary in both directions.
  • Duplicate submission from a flaky client, asserting one reservation and one charge.
  • A cached price that has gone stale, asserting the guest is never charged more than they were shown.

A fixed channel buffer caused twelve incidents a month

A property group came to us with what they described as an overbooking problem of about twelve incidents a month across forty properties. That was small enough to absorb, and large enough to be a standing item in an operations meeting.

The cause was not concurrency at the database, which was correct. The cause was the channel buffer. It had been set to a fixed two units per room type per property years earlier by somebody who had since left.

At a small property with four rooms of a type, two units of buffer is prudent. At a large one with ninety, it is meaningless. The propagation delay to one particular agency was long enough to sell through it during a promotion.

We made the buffer proportional, exposed it to revenue managers with the propagation delay shown next to it, and added the front desk alert. Incidents went to under one a month, and the remaining ones were deliberate.

The right target is zero accidental double bookings

Zero accidental double bookings, at any volume, is achievable and it is the right target. Zero overbookings can be the wrong target, because overbooking may be a commercial choice. The target is zero that nobody chose.

It is a good target because it is binary and unarguable. Either your system can produce a state nobody decided on, or it cannot. The work to move from the first to the second is measured in weeks rather than quarters.

More on how we work in this sector on our travel and hospitality page.

Written by Brilliant Systems

Our engineers write these between projects. If something here is relevant to a decision you are making, we are happy to talk it through without it becoming a pitch.

Certified, partnered and awarded