- Blog
- Why “Unavailable” Isn’t Enough
Why “Unavailable” Isn’t Enough
A bare 'unavailable' hides the reason, the boundary, and the recovery. How reason codes, validation boundaries, and last-known-good data prevent real damage.
Salman Farooqi7 min read

Every booking system eventually has to say no. The question is not whether it will happen, but how well the system explains it. A bare "unavailable" is the cheapest answer to build and the most expensive to operate. It tells the user nothing, tells the support team nothing, and — worst of all — hides the difference between "this genuinely cannot happen" and "we do not actually know."
This post is about why "unavailable" is not enough: what reason codes give you, where validation boundaries belong, how last-known-good data keeps a system honest during an outage, and the operational and user damage that good design prevents.
The failure scenario
Imagine a customer trying to book a cabin for a specific weekend. The system returns "unavailable." The customer tries another date, then another, then gives up and books elsewhere. Meanwhile, the support team gets a ticket: "Why can't I book this cabin?"
The answer turns out to be one of several very different things:
- The cabin is genuinely sold out for that weekend.
- The cabin is closed for maintenance and not bookable at all.
- The availability service timed out, and the system returned "unavailable" as a default because it had no better answer.
Three completely different realities, all rendered as the same two words. The customer cannot act on it, support cannot resolve it without a manual investigation, and the business cannot tell whether it is losing bookings to real demand or to a silent outage.
Reason codes: make the no explainable
The first fix is to stop returning a bare boolean and start returning a reason. Instead of available: false, the system should answer with a code that names the cause:
type AvailabilityReason =
| { status: "available" }
| { status: "sold_out"; nextAvailableDate?: string }
| { status: "closed"; reopenDate?: string }
| { status: "unknown"; retryAfterSeconds?: number };
Each branch changes what the user sees and what the system does next:
sold_out— the item is real and bookable, just full. Offer the next available date.closed— the item is not bookable at all. Say why, and when it reopens.unknown— the system does not know. Say so, and offer a retry, rather than pretending.
The unknown branch is the one most systems skip, and it is the one that prevents the most damage. When you cannot answer, the honest response is to say you cannot answer — not to fabricate a definitive "no."
Validation boundaries: decide where the truth lives
Reason codes only work if the system knows which layer is allowed to decide. A validation boundary is the explicit line that says: this layer checks the business rules, and this layer does not. Without it, every layer guesses, and the guesses disagree.
A clean boundary looks like this:
- The catalog knows what exists.
- The availability engine knows what is free, and owns the reason codes.
- The booking service knows what can be committed, and re-checks before committing.
- The UI renders the reason the engine returned — it does not invent its own.
The boundary matters because it prevents the UI from translating a real reason into a vague message, and it prevents the booking service from trusting a stale answer at the moment of commitment. Each layer has one job, and the reason code is the contract between them.
The boundary contract
The reason code is the contract that keeps the layers honest. It is a typed, documented shape that every layer agrees on — not a string that each layer reinterprets. When the contract is explicit, a new consumer (a mobile app, a partner API, a support tool) can rely on the same codes instead of inventing its own. The cost is that the contract must be versioned and maintained; the payoff is that the truth has one owner and one vocabulary.
Last-known-good data
During an outage, the availability engine may not be able to answer at all. The temptation is to fail closed and return "unavailable" for everything — which, as the failure scenario shows, is indistinguishable from a real answer and can silently destroy bookings.
Last-known-good data is the alternative. The system keeps the most recent successful availability snapshot and, when the live check fails, serves that snapshot with an explicit caveat: "availability may be out of date." This is honest — it does not claim to be current — but it keeps the product usable and keeps the customer moving instead of dead-ending.
The trade-off is real: a stale snapshot can over-promise. The mitigation is to mark the data as stale and to re-check at the moment of commitment, so the final decision is never made on stale data.
Reason codes turn a dead end into a decision. The difference between "unavailable" and "sold out, next date is the 14th" is the difference between losing a customer and keeping one.
The trade-offs, explicitly
Every improvement here has a cost, and pretending otherwise is how systems get over-engineered:
- Reason codes add surface area. More states mean more to test, more to translate, and more to get wrong. The payoff is that the states you do have are meaningful.
- Validation boundaries add indirection. A clean boundary is more code than a single "check availability" call. The payoff is that the truth has one owner instead of many.
- Last-known-good data risks staleness. Serving a snapshot can over-promise. The payoff is that an outage does not silently become a total loss of bookings.
- Honest "unknown" can feel like a downgrade. Telling a user you do not know is less satisfying than a confident answer. The payoff is that you never present a guess as a fact.
The framework is not about adding complexity for its own sake. It is about making the system's answers trustworthy — and trust is the thing that is actually on the line.
A checklist for explainable availability
A practical starting checklist for any system that has to say no:
- Every "unavailable" carries a reason code, not just a boolean.
- The
unknowncase is handled honestly, with a retry, not disguised as a real answer. - Each layer has a defined validation boundary and does not invent its own reasons.
- The booking service re-checks availability at the moment of commitment.
- Last-known-good data is served with an explicit staleness caveat during outages.
- Support can resolve a "why can't I book this?" ticket without a manual investigation.
Why this matters
The damage a vague "unavailable" causes is rarely visible in a single request. It shows up as abandoned bookings, support tickets, and a slow erosion of trust — the customer who cannot tell whether the product is full or broken. Reason codes, validation boundaries, and last-known-good data are the small, boring investments that keep a system honest when it has to say no. That honesty is what separates a platform that feels reliable from one that quietly isn't.
Next steps

Elhaam Basheer Chauhdry
CTO, TrueDevs
A conversation with Elhaam
Start a conversation about your project
We’ll look at what’s not working, what’s getting in the way, and where the next useful move is.
Book a call with Elhaam
We’ll look at what’s not working, what’s getting in the way, and where the next useful move is.
Related articles

Booking and Commerce
Availability Should Shape Product Discovery Before Checkout
Why availability is a discovery problem, not just a checkout problem — and a decision framework for building availability-first search, filtering, and booking.
Elhaam Basheer Chauhdry6 min read

Growth Engineering
Stop Optimising the Wrong LCP Asset
How to identify the asset that actually drives your Largest Contentful Paint, tie it to intent and conversion, and run a before/after diagnostic.
Noman Bin Bashir6 min read