Back in June I wrote about how Lueira handles the moment someone actually commits to a booking: instructor, equipment and time slot locked together in one atomic transaction, or not at all. That post was about the commit. This one is about everything that has to be true before the commit makes sense — the number staring back at a customer, or at a member of staff, that says a resource is "available" or "booked" at a given time. That number turns out to be one of the hardest things to get consistently right in the whole platform. In some ways it's harder than the booking transaction itself, because unlike a transaction, it never sits still.
Availability is not a fact, it's a moving target
The instinct when you first model this is to treat availability as a property you can read off a record: this ski instructor is free at 10am, this pair of skis is in stock, this slot in a group lesson has a seat left. In practice, "available" is an aggregate computed from several independent, concurrent sources of truth, and any one of them can flip the answer:
- A staff member manually blocks a slot in the backoffice calendar, because an instructor called in sick or needs to leave early.
- A customer completes an online booking through the store, consuming the last seat or the last pair of boots in a size.
- An item gets marked broken or sent for repair mid-morning, pulling it out of the rentable pool.
- Someone edits the recurring weekly schedule template — moving a class, changing capacity, adding a new time slot — which ripples forward into every future week that inherits from it.
- A hold from an abandoned checkout expires, releasing a slot or item that looked committed for the last ten minutes but never actually converted into a booking.
On a quiet Tuesday these rarely collide. On a busy Saturday morning at a rental counter, with the online store also taking bookings and an instructor rearranging their day, several of these can happen within the same second. The system has to produce one consistent answer regardless of which of these five paths asked the question last.
Query live, don't trust a cache to keep up
The tempting shortcut is a precomputed availability table: one row per resource per time slot, updated whenever something changes, read cheaply by the storefront. It's tempting because it's fast, and because it feels like the natural shape of the data. The catch is that it needs to be invalidated correctly from five independent write paths, and it only takes one of those paths missing an update, or racing another one, for the number on screen to be wrong. And wrong in this domain isn't cosmetic: it means overbooking an instructor, or telling a customer a rental is available when it just got marked broken.
My default reasoning here has been to treat availability as something you compute on read against current state, rather than something you store and hope stays in sync. A live query — join the resource's calendar, its current bookings, its equipment status, the active holds — is more expensive per request than a cache lookup, but it is correct by construction: there's no invalidation logic to get wrong, because there's nothing sitting stale to invalidate. For a query that mostly touches a single day's worth of resources and bookings, that cost is usually manageable long before it becomes the bottleneck.
If you cache anything, cache narrowly
That doesn't mean caching is off the table everywhere — a public storefront with real traffic will eventually need some layer of caching in front of the live query. But the failure mode I'd worry about most is a cache whose invalidation trigger is too broad: something changes, so the whole thing gets cleared and rebuilt. That's simple to write and it quietly reintroduces every one of the correctness problems a precomputed table has, just with extra steps.
The alternative is to key the cache narrowly enough that an invalidation trigger only has to reason about the one resource and time window that actually changed, illustrated here as simplified pseudocode rather than real production code:
// naive: any write anywhere clears everything
onAnyBookingWrite(event):
cache.clear() // correct, but throws away everything else too
// narrow: invalidate only the affected resource/day
onInstructorAvailabilityChanged(event):
key = availabilityKey(event.instructorId, event.date)
cache.delete(key)
onBookingConfirmed(event):
key = availabilityKey(event.resourceId, event.date)
cache.delete(key)
onEquipmentStatusChanged(event):
key = availabilityKey(event.itemId, event.date)
cache.delete(key)
onHoldExpired(event):
key = availabilityKey(event.resourceId, event.date)
cache.delete(key)
The difference isn't just performance. A narrow trigger is something you can reason about and test per write path — "does marking equipment broken correctly invalidate the right key" is a question with a clear yes or no answer. "Does clearing the whole cache always happen in time" is a question about timing and race conditions across five unrelated code paths, which is a much harder thing to be confident about.
One truth, three windows
The other thing that makes this problem sharp in practice is that availability isn't asked once — it's asked from at least three different places that need to agree: the public storefront where a customer is booking a rental or an activity, the backoffice calendar where staff manage the day, and internal reporting views used to plan capacity. If those three ever disagree, even briefly, someone notices. A concrete case that comes up constantly: an instructor decides mid-morning, for a personal reason, to mark themselves unavailable for the rest of the day. That has to reach the online store within seconds — not at the next scheduled sync — or the store will happily sell a private lesson with an instructor who already left. The backoffice knows immediately, because that's where the change was made; the hard part is making sure the storefront's answer to "is this instructor free at 2pm" is derived from the exact same state, not a snapshot from twenty minutes ago.
This is, in the end, the real argument for computing availability live rather than maintaining separate precomputed views per surface: if storefront, backoffice and reporting all resolve availability through the same query against the same current state, there's structurally nowhere for them to disagree. Any caching layer added later has to preserve that property, not work around it.
None of this is exotic engineering — there's no clever trick that makes concurrency and multiple writers disappear. What actually helps is being honest about how many independent things can change "available" out from under you, and designing so the system asks the question fresh rather than trusting an answer it computed a moment ago. Getting the booking transaction right, which I wrote about in June, matters a lot. Getting the number that leads someone to attempt that transaction in the first place matters just as much, and it's the piece that's easiest to get subtly wrong without noticing for weeks.
