Availability Engines: How to Stop Double Bookings, Time-Zone Bugs and Overselling
All Articles
SaaSBooking EngineArchitecture

Availability Engines: How to Stop Double Bookings, Time-Zone Bugs and Overselling

HR
Hassan Raza
Tech Lead
Jul 20, 2026 10 min read

The Bug That Only Appears When You Succeed

Availability logic has a cruel property: it works perfectly during development, passes every test, survives the first months of production, and then fails on the busiest day of the year.

The reason is that almost every availability bug is a concurrency bug, and concurrency bugs need traffic to appear. Two customers clicking Book within the same 200 milliseconds is impossible to hit by hand and inevitable at scale. So the failure arrives precisely when the platform is doing well, in front of the most customers, with the least slack in the operation to absorb it.

This article covers how to build an availability engine that does not do that — for booking SaaS, parking, restaurants, clinics, rentals, and anything else that sells a finite resource against time. It comes from building and operating exactly these systems, including the multi-tenant booking platform behind BookFlow Pro.

Availability Is Not a Query, It Is a Ledger

The natural first implementation counts existing bookings and subtracts from a capacity number. Something like: find every booking overlapping this window, sum the units, compare to the total.

It is correct in isolation and wrong as an architecture, for three reasons.

It gets slower forever. The query scans a table that grows without bound, and the date-range overlap condition is exactly the kind of predicate that indexes handle poorly. A search that takes 40 milliseconds with 10,000 bookings can take several seconds with two million.

It cannot be locked meaningfully. To make the check-then-write sequence safe, you would have to lock every row it read, which under any real load produces either lock contention that stalls the site or a lock scope so broad that bookings serialise behind each other.

It has no place to record anything else. Overbooking allowances, holds, blackout dates, and channel-specific inventory all need somewhere to live, and a derived count has nowhere to put them.

The alternative is an explicit inventory ledger: a row per resource, per day (or per slot), holding the total units, the units consumed, and the units held. Bookings write to it; searches read from it. It is denormalised on purpose, and that is the point.

Picking the Right Granularity

The unit of your inventory table determines what you can express.

  • Per day. Right for stays — hotels, parking, equipment hire. A booking from the 14th to the 21st consumes one unit on each of seven day rows.
  • Per slot. Right for appointments — clinics, salons, classes, restaurant covers. A slot is a specific start time with a specific capacity.
  • Per resource per slot. Right when the individual resource matters, such as a named practitioner, a specific court, or a particular vehicle.

The mistake worth avoiding is choosing per-slot granularity for a stay-based business because slots feel simpler. A car park booked from Friday afternoon to the following Thursday morning is not a slot; modelling it as one produces an availability engine that cannot answer the question customers actually ask.

For businesses that need both — a venue that hires rooms by the hour and also by the day — model the finer granularity and derive the coarser one, never the reverse.

Making the Write Safe

This is the core of the whole subject. The sequence "read availability, decide it is fine, write a booking" is unsafe unless the read and the write are protected together.

Two approaches work, and both are fine.

Pessimistic locking. Inside a database transaction, select the relevant inventory rows with a row-level lock, check the remaining units, decrement, insert the booking, commit. A second concurrent request blocks at the select until the first commits, then reads the updated value and correctly finds nothing available. This is straightforward, easy to reason about, and what we use most often.

Optimistic concurrency. Perform a conditional update that only succeeds if sufficient units remain, and treat zero affected rows as a failure to be retried or reported. This avoids holding locks but needs disciplined handling of the failure case at every call site.

What does not work, regardless of how it is dressed up: checking availability in application code, then writing in a separate statement, without either mechanism. Nor does an application-level mutex if you run more than one server process, which you will.

Two practical details matter. Always acquire locks on inventory rows in a consistent order — ascending by date is the obvious choice — because a booking for the 14th to the 16th and another for the 16th to the 14th acquiring locks in opposite orders is a textbook deadlock. And keep the transaction short: never call a payment provider inside it, because holding inventory locks across a network round trip to an external API will bring the system to a standstill at exactly the wrong moment.

Holds: Reserving Without Committing

That last point creates an obvious problem. If you cannot hold locks across the payment call, what stops someone else taking the space while the customer is entering their card details?

The answer is a hold: a short-lived, expiring claim on inventory that is neither available nor booked.

The mechanics are simple. When a customer enters checkout, decrement available units and record a hold row with an expiry a few minutes out. If payment succeeds, convert the hold into a booking. If payment fails or the customer abandons, the hold expires and the units return.

The details that determine whether it works:

  • Expiry length should reflect the real checkout duration. Too short and customers lose their space mid-payment; too long and abandoned checkouts starve inventory on peak dates. Five to fifteen minutes suits most flows.
  • Expire holds actively with a scheduled job, and also treat expired holds as unavailable in reads. Relying only on a background job means a crashed worker silently removes inventory from sale.
  • Tell the customer. A visible countdown makes the constraint legible and, as a side effect, raises checkout completion.
  • Make hold release idempotent. A hold that expires and is also cancelled by an abandoned-checkout handler must not return its units twice.

Holds are what make it possible to keep transactions short and still guarantee that a customer who reached the payment screen can complete.

Overbooking on Purpose

Not every business wants availability to stop exactly at capacity. Airlines, hotels, and car parks routinely sell beyond physical capacity because a predictable share of customers do not turn up.

If you support this, make it explicit rather than emergent:

  • An allowance configured per resource, per product, and ideally per date range, since no-show rates differ sharply between a wet Tuesday and Christmas week.
  • Visible consumption of the allowance, so an operator can see they are 12 sold beyond physical capacity rather than discovering it on the day.
  • Derived from measured no-show data where possible, not guessed. This requires actually recording no-shows, which many systems never do.

Overbooking without measurement is gambling. Overbooking with a year of no-show data is yield management.

Time Zones and the Bugs That Live There

More availability bugs come from time handling than from concurrency, they are just less dramatic.

The rules that prevent nearly all of them:

  • Store every timestamp in UTC, without exception.
  • Store the resource's time zone as a first-class attribute. A booking platform serving multiple locations cannot infer it from the server, the user, or the browser.
  • Convert to local time only at the edges — when displaying, and when interpreting user input.
  • Store recurring rules in local time, not UTC. A clinic that opens at 9am opens at 9am after the clocks change, and a rule stored as 08:00 UTC will be wrong for half the year.
  • Handle the clock-change days deliberately. In the UK, one day in the year has a 1am hour that occurs twice and one has a 2am hour that does not exist at all. Slot generation across those days will produce duplicates or gaps unless it is written with them in mind.
  • Define what a "day" means for cutoffs and daily capacity in the resource's local time zone, not the server's.

A related trap is the all-day or date-only value. A booking date of "14 March" is not a timestamp and should not be stored as one; converting it through a time zone at some later point will eventually turn it into the 13th.

Buffers, Lead Times and the Rules Around the Edges

Raw capacity is rarely the whole constraint. Real businesses have rules about when a slot can be sold at all:

  • Setup and cleardown buffers, so a 60-minute appointment consumes 75 minutes of the resource.
  • Minimum lead time, blocking a booking for 20 minutes from now when the business needs two hours' notice.
  • Maximum booking horizon, since a business that has not set next summer's prices should not be selling next summer.
  • Minimum and maximum stay lengths, often varying by season.
  • Cutoff times, such as no same-day bookings after 4pm.
  • Blackout dates for closures and maintenance.

Every one of these needs to be applied consistently in three places: the search that produces available options, the validation that runs at booking time, and any amendment flow. Rules enforced only in the search are trivially bypassed by anyone who keeps a stale page open, and amendment flows that skip validation are how impossible bookings get into a system that otherwise looks airtight.

Amendments Are Where Correctness Goes to Die

Changing an existing booking is the single most common source of inventory corruption, because it is a release and an acquire that must both succeed or both fail.

A customer extends a stay by two days. The naive implementation adds two day rows and updates the booking. If the extension fails partway, or if the two new days are not available, the system can end up with inventory decremented for days the booking no longer covers, or a booking spanning days it never claimed.

Do the whole amendment in one transaction with the same locking discipline as a new booking: release the old range, acquire the new range, update the booking, commit or roll back entirely. And run a reconciliation job that periodically compares the inventory ledger against the sum of live bookings, reporting drift. You want to hear about a discrepancy from your own monitoring, not from an operator whose car park is full.

Testing Something That Only Breaks Under Load

Ordinary unit tests will not find these bugs. What does:

  • Concurrency tests that fire many simultaneous booking attempts at a resource with a single unit available and assert that exactly one succeeds. This test is worth more than any other in the suite.
  • Time-zone tests pinned to the actual clock-change dates for each region you serve.
  • Property-based or fuzz testing over overlapping date ranges, which reliably finds the boundary condition where a checkout on the final day of a stay is handled inconsistently.
  • A reconciliation assertion in integration tests: after a randomised sequence of bookings, amendments, and cancellations, the ledger must exactly match the bookings.
  • Load testing at realistic seasonal peaks rather than average traffic, because average traffic proves nothing about the day that matters.

The Short Version

Keep an explicit inventory ledger rather than deriving availability from bookings. Lock it properly on every write, in a consistent order, in short transactions. Use expiring holds to cover the payment step. Store everything in UTC with an explicit resource time zone. Apply booking rules in search, in validation, and in amendments alike. Treat amendments as atomic release-and-acquire operations. Reconcile continuously, and write the concurrency test.

Every one of those points exists because we have seen the alternative fail in production. If you are building a booking platform and want the availability model reviewed before it meets a bank holiday weekend, our contact page is the fastest way to reach us.

Looking to build something in Booking & Reservation SaaS?

See how we can help
Share this article