Hotel PMS
Property operations, room inventory, reservations, folios, and housekeeping across time zones.
Product overview
Run hotel operations across reservations, room inventory, check-in, housekeeping, folios, and night audit while preserving property-local business dates.
Main actors
Guest, Front desk, Housekeeping, Manager.
Assumptions
- Each property is a tenant boundary
- Room type inventory is sold before a physical room is assigned
- Business date may differ from UTC date
- External channels deliver at-least-once updates
Non-goals
- Building a public travel marketplace
- Card vaulting
- Door-lock firmware
- Revenue-management optimization
Requirements
Must-have
- Rate-plan and room-type inventory
- Reservation lifecycle
- Room assignment and housekeeping
- Folio charges/payments
- Property business date and audit trail
Should-have
- Channel-manager integration
- Group blocks
- Room moves
- No-show workflow
Future scope
- Revenue management
- Multi-property guest profile
- Digital keys
- Loyalty
Non-functional decisions
| Concern | Decision |
|---|---|
| Consistency | Inventory, room assignment, and folio postings are transactional within a property. |
| Availability | A property can continue core front-desk work during nonessential integration outages. |
| Auditability | Financial changes are reversals plus new entries, never edits in place. |
| Timezones | Store instants in UTC and separately record property timezone and local business date. |
| Tenant isolation | Every property-owned row and authorization path carries property_id. |
User roles
| Role | Access boundary |
|---|---|
| Front desk | Property reservations, rooms, and guest-facing folio actions |
| Housekeeping | Assigned room status; no folio/payment data |
| Manager | Property configuration, overrides, and audit |
| Finance | Folio, settlement, and reports; no room-key actions |
Core workflows
Domain model
| Entities | Relationship |
|---|---|
Property → RoomType → Room | Type is sellable inventory; room is physical assignment |
Reservation → Stay | Reservation commitment becomes an operational stay |
Stay → RoomAssignment | Date-bounded history supports moves |
Reservation → Folio → FolioEntry | Append-only charges, payments, and reversals |
Property → BusinessDate | Night audit advances the accounting day |
Database design
PostgreSQL remains the transactional source of truth. Add specialized stores only for the workloads called out below.
reservations — Guest booking commitment
room_type_inventory — Per-night sellable count
room_assignments — Physical room history
folio_entries — Append-only money movements
Transaction and retention boundaries
- Lock all room-type inventory dates in ascending order before converting hold to sale
- Assign room only if clean and no overlapping assignment
- Night-audit charge source key is unique per stay/date/type
- Reservations are cancelled, folio entries reversed; PII may be anonymized by retention policy
API design
| Endpoint | Purpose and permission | Idempotency / status |
|---|---|---|
GET /v1/properties/:id/availability?from=&to= | Read room-type availability Property member or public booking scope | Not required 200, 422 |
POST /v1/reservations | Hold inventory and confirm booking Booking client/front desk | Required 201, 409, 422 |
POST /v1/stays/:id/check-in | Assign room and activate stay Front desk | Transition id 200, 409 |
PATCH /v1/rooms/:id/housekeeping | Update clean/inspected state Assigned housekeeping or manager | Not required 200, 409 |
POST /v1/folios/:id/entries | Post charge, payment, or reversal Front desk/finance by entry type | Required source key 201, 409, 422 |
POST /v1/properties/:id/night-audit | Advance business date Manager with step-up | Business date is the key 202, 409 |
List endpoints use opaque keyset cursors unless a small bounded collection is explicit. Unknown objects return 404 where 403 would leak existence.
Architecture
System context
Main application architecture
Critical workflow sequence
Event model
| Event | Producer → consumers | Delivery |
|---|---|---|
ReservationConfirmed | Reservation module → Email, channel sync | Outbox and dedupe by reservation/version |
RoomStatusChanged | Housekeeping → Front desk projection | Versioned last-write projection |
BusinessDateAdvanced | Night audit → Reports, integrations | Replayable; report output keyed by property/date |
Failure modes
Double booking
Failure
Concurrent channels sell the last room type.
Mitigation
Lock inventory rows for every stay date and enforce held + sold <= total.
Duplicate channel message
Failure
Partner resends a reservation.
Mitigation
Unique partner/property external id and return prior result.
Dirty room assignment
Failure
Room state changes during check-in.
Mitigation
Lock room and validate clean state inside assignment transaction.
Night audit crash
Failure
Audit stops after some charges.
Mitigation
Idempotent source keys and checkpointed phases allow resume without duplicates.
DST boundary
Failure
Local time repeats or skips.
Mitigation
Business date is explicit; convert instants with IANA timezone and never derive audit date from server locale.
Payment terminal offline
Failure
Authorization status is unknown.
Mitigation
Record pending external operation; reconcile rather than posting an assumed payment.
Consistency and concurrency
Strong consistency / transactions
- Nightly room-type inventory
- Physical room assignment
- Folio append
- Business-date advance
Eventual convergence
- Channel availability push
- Confirmation email
- Portfolio analytics
- guest-profile merge
Concurrency controls
- Lock inventory rows in stable date order
- PostgreSQL exclusion constraint prevents overlapping room assignment
- Optimistic version protects reservation edits
- Unique source keys make night-audit postings idempotent
Caching and background work
Safe to cache
- Property configuration by version
- Room descriptions and media
- Public rate display for short TTL
Never a cache authority
- Check-in room eligibility
- Authoritative inventory during confirm
- Folio balance used for checkout
Tenant-keyed cache entries; invalidate configuration changes. Booking and operations re-read PostgreSQL.
Background jobs
- Expire holds
- Send confirmations
- Push channel availability
- Run resumable night audit
- Reconcile terminals
- Anonymize guest PII after retention
Security boundaries
Security claims here are design controls, not certification. Threat modeling and jurisdiction-specific review remain required.
- property_id derived from membership, never trusted from body
- Role and object checks for rooms, reservations, folios
- Step-up and dual-control policy for sensitive refunds/audit override
- Tokenized payment methods and segmented terminal integration
- Immutable audit of guest-record access and financial changes
- Rate-limit public availability and confirmation-code lookup
- Mask identity fields from housekeeping views
Observability
Logs
- Reservation, room assignment, folio reversal, and business-date transition with property and actor ids
- Channel message external id, dedupe result, and inventory version
- Night-audit phase checkpoint and idempotent posting outcome
Metrics
- Room-type inventory conflicts and oversell prevention by property/date
- Channel availability sync lag and rejection rate
- Night-audit phase duration, restart count, and properties not advanced
- Dirty-room wait time and unresolved folio balance count
Traces
- Trace reservation from channel/public API through nightly inventory locks
- Trace check-in across room eligibility, incidental authorization, and folio activation
Alerts
- Property inventory invariant violation
- Night audit misses property cutover window
- Channel sync lag risks selling stale availability
Testing strategy
| Layer | What it proves |
|---|---|
| Unit | Stay-date ranges, DST cases, folio reversal, state machines |
| Integration | Inventory locks, exclusion constraints, tenant policies, audit idempotency |
| Contract | Channel and terminal message fixtures |
| Concurrency | Last-room channel race and simultaneous room assignment |
| Failure | Audit phase crash, offline terminal, late cancellation |
| End-to-end | Reserve → check-in → move → charges → checkout → night audit |
Do not mock PostgreSQL constraints, transaction isolation, object-store semantics, or provider contract fixtures in the tests intended to prove those boundaries.
Deployment evolution
Scale evolution
These are architecture scenarios, not throughput claims. Each transition should follow measured workload and reliability pressure.
| Scenario | Architecture may evolve toward | Decision rule |
|---|---|---|
| 1K users | One API, PostgreSQL with property-scoped rows, a channel-sync worker, and a scheduled night audit | Guard each property's business date and room-night inventory; night audit runs exactly once per property and date |
| 100K users | Scale channel and reporting workers separately; route all property writes to one primary owner | Separate channel-manager and reporting workers when OTA sync lag or report queries slow front-desk operations |
| 1M+ users | Partition by property cohort/region; keep night audit and inventory single-writer per property; replicate sanitized events to analytics | Move property cohorts into separate cells for isolation or noisy-neighbour load; a property never spans cells |