Hard problem 01
Two guests, one last room
The check-then-act bug is almost impossible to see in code review, because the code reads correctly: look up availability, confirm a room is free, create the booking, decrement inventory. Every line is right. The problem is the gap between line one and line four.
Two requests land microseconds apart. Both read availability as one. Both conclude they may proceed. Both write. The room is sold twice, and nobody finds out until check-in.
Why I chose pessimistic locking
Optimistic locking — a version column, retry on conflict — is usually the right default, and it's what I'd reach for on a low-contention table. Booking inventory is the opposite case: contention is concentrated exactly where the resource is scarce, so the last room is precisely the row every concurrent request wants. Under optimistic locking that row generates a storm of conflicts and retries at the worst possible moment.
So the booking path takes a pessimistic write lock on the inventory rows for the requested dates. The second request blocks at the database rather than racing, and when the lock releases it re-reads reality: zero rooms, request rejected. Inventory is then adjusted with an atomic bulk update — a single statement across the date range, guarded by a condition that rooms remain — so the decrement can't be based on a stale count.
The trade is deliberate: some throughput, in exchange for never overselling. For inventory that's the correct side of the trade.
B never sees stale availability. It waits at the lock, reads the world after A committed, and fails honestly.
Hard problem 02
Pricing rules that don't turn into a nest of conditionals
Dynamic pricing starts simple and rots fast. A weekend multiplier, then a surge factor for high occupancy, then a seasonal adjustment, then a minimum-stay discount — and the pricing method becomes four hundred lines of nested conditions where nobody can tell which rule won.
I built it with the decorator pattern instead. There's a base rate, and each rule is a wrapper that takes a price and returns a price. Composing them is composing objects, not editing a function. A new rule is a new class, and removing one is deleting it — neither touches the booking path.
Why it's recomputed on a schedule
Pricing depends on occupancy, which changes as bookings land, so a quote is only as fresh as its inputs. Computing it per request would put a chain of decorators and an occupancy query in front of every search response. Instead a scheduled job runs hourly, recomputes prices across the inventory, and writes them down — so reads are cheap and search stays fast.
The cost is a staleness window: for up to an hour, a displayed price can lag reality. That's acceptable for display, so the final price is re-validated at booking time before payment.
The rest of it
- Spring Security with JWT for stateless auth, so nothing depends on server-side session affinity.
- Stripe for payments, with the booking only confirmed once payment succeeds — inventory is held, not sold, until then.
What I'd do next
- Add an expiry to inventory holds, so an abandoned checkout returns the room automatically.
- Load-test the lock path to find where queueing becomes a latency problem rather than assuming it doesn't.
- Keep a price history table, since right now a recomputation overwrites what a guest was quoted.