Case study // 10
A multi-branch hotel reservation system, built around the rules a front desk actually runs on.
// CONTEXT
A hotel chain running several branches needs one system that four different people trust: a guest booking from home, a clerk working the desk, a manager reading last night's numbers, and an admin moving between branches. The interesting part is not the booking form. It is that a reservation is a six-state object with money attached, and almost every rule that makes it a hotel system rather than a CRUD app lives in the gaps between those states.
// CONSTRAINTS
// DECISIONS
That is how hotels actually work, and it is why an overbooked night is a capacity problem rather than a collision on a specific door number. It also means housekeeping and maintenance can shuffle rooms right up to arrival without touching a single reservation.
Assigning a room at booking time, which looks tidier in the schema and forces a reshuffle of live reservations every time a room goes out of service.
rooms.status is a right-now flag: it says a room is free this second and nothing about next Tuesday. Counting active reservations against branch inventory over the requested dates is the only answer that holds for a future booking, and putting it in one place meant all five booking paths could be moved onto it. Overlap is deliberately strict, so a stay ending the morning another begins does not conflict — that is correct hotel semantics, and it is pinned by a test.
Checking rooms.status at booking time, which is what the system did before and is why it could be double-booked.
The 19:00 auto-cancel only needs to know whether a guarantee exists, not what it is. Once that was clear, holding the number had no upside and unbounded downside. Rows that predated the change were masked in place by an intentionally irreversible migration.
Encrypting the full number at rest, which still leaves a database worth stealing and a key worth stealing with it.
Every other booking path requires arrival after today, which is right for a website and wrong for a guest standing at the desk. A separate path creates the reservation already checked in with the room assigned and the folio open, in one transaction, while still enforcing the occupancy cap and — importantly — capacity for the whole stay, because a room free tonight says nothing about Thursday.
Dropping the after:today validation globally, which would let a customer book yesterday from the public site.
// OUTCOME
// REFLECTION
The controllers are fat and the business rules are duplicated across them — the four-weeks-to-one-month rewrite exists in four places, and calculateTotal in two. That was fine until it wasn't: the first bug I fixed in one copy was still live in the other three. A rules layer should have arrived the second time a rule was pasted, not the fourth. The repository keeps a written gap analysis of what this is still missing against a real PMS, which I would rather hand over than pretend does not exist.