Problem & Challenge
Bugs in hotel software cost real money. A guest billed twice means refunds and complaints, a room sold twice means an overbooked night, and one property seeing another's data is a privacy breach.
Serene Management serves 12 staff roles across one or more hotels, and every night an audit has to close the books correctly.
Rules Before the First Line
- Thin routes, rules in services: route handlers authorize, validate and scope; business rules live in domain services.
- Property scope from the server: the property a request can touch comes from the session, never from the client.
- Exact money: amounts are stored exactly, never as floats.
- Append-only ledgers: financial records are never edited; corrections add a row.
- Slow work goes to a worker: long jobs never depend on a browser staying open.
These rules are enforced by lint and tests, so they held across 127 database models.
Architecture
A second, background path handles night audit: the job is queued as a row in PostgreSQL, a worker claims it, the business date rolls over, charges are posted once, and room boards refresh through database notifications. A hidden button is never the only protection; every action is authorized on the server.
Key Decisions
1. Retries can't double-charge
Payments and other financial actions carry an idempotency key. If a response is lost and the client retries, the server returns the first result instead of recording a second payment. An acceptance test sends the same payment twice and checks that one is recorded.
2. The books close in a worker
A worker claims the night-audit job with FOR UPDATE SKIP LOCKED, runs it under a lease and retries up to three times. Postings are held while the date closes. Duplicate night charges in testing: zero.
3. Offline is read-only, on purpose
When the connection drops, the front desk can still see arrivals, in-house guests and departures, but cannot book, check in or take payment. That avoids the hardest problem in hotel software: merging conflicting offline bookings.
Performance Results
Measured before optimizing. One innocent-looking ORM count was scanning every reservation room on each request.
| Bottleneck | Before | After |
|---|---|---|
| Global search | 8.3 s | 0.45 s |
| Guest statistics | 168 ms | 0.16 ms |
| Room board (buffers read) | 38,346 | 4,967 |
| List page (ORM count) | ~1 s | Fixed |
Slow list queries were holding the connection pool, so simple auth checks rose from 8 ms to 550 ms. Before the fixes the server saturated at 10–25 requests per second; after four fixes it handled 50 requests per second with 0 errors and a P95 latency of 192 ms. Measured on one instance on a 4-core laptop.
Auditing My Own Work
A pre-release audit found a release blocker: a routine rate-plan change could silently re-bill nights that had already been charged. Nothing crashed; guests would simply have paid twice.
- P0: silent re-billing, fixed and covered by tests.
- P1: no request size limit, now capped at 1 MiB.
- P1: the offline page exposed guest lists, now fixed.