AdvancedMulti-tenancyBookingsTimezonesAudit logsTransactions

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

ConcernDecision
ConsistencyInventory, room assignment, and folio postings are transactional within a property.
AvailabilityA property can continue core front-desk work during nonessential integration outages.
AuditabilityFinancial changes are reversals plus new entries, never edits in place.
TimezonesStore instants in UTC and separately record property timezone and local business date.
Tenant isolationEvery property-owned row and authorization path carries property_id.

User roles

RoleAccess boundary
Front deskProperty reservations, rooms, and guest-facing folio actions
HousekeepingAssigned room status; no folio/payment data
ManagerProperty configuration, overrides, and audit
FinanceFolio, settlement, and reports; no room-key actions

Core workflows

Reserve
Search rate and room-type availability → Create short hold → Collect guarantee token → Confirm reservation and consume inventory → Send confirmation
Check in
Verify identity and reservation → Assign clean compatible room → Authorize incidentals → Create/activate folio → Set room occupied
Night audit
Freeze property business date → Post recurring room charges → Reconcile unsettled transactions → Produce audit report → Advance business date → Resume writes

Domain model

EntitiesRelationship
Property → RoomType → RoomType is sellable inventory; room is physical assignment
Reservation → StayReservation commitment becomes an operational stay
Stay → RoomAssignmentDate-bounded history supports moves
Reservation → Folio → FolioEntryAppend-only charges, payments, and reversals
Property → BusinessDateNight 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

reservations ------------ id UUID PK property_id UUID FK confirmation_code TEXT room_type_id UUID FK arrival DATE departure DATE status TEXT version INT Constraints: UNIQUE(property_id, confirmation_code); departure > arrival Indexes: (property_id, arrival, status); (property_id, guest_id)

room_type_inventory — Per-night sellable count

room_type_inventory ------------------- property_id UUID room_type_id UUID stay_date DATE total INT held INT sold INT Constraints: PK(property_id, room_type_id, stay_date); held + sold <= total Indexes: primary key supports range lookup

room_assignments — Physical room history

room_assignments ---------------- id UUID PK property_id UUID stay_id UUID FK room_id UUID FK from_at TIMESTAMPTZ to_at TIMESTAMPTZ NULL Constraints: exclusion constraint prevents overlapping active room assignment Indexes: (property_id, room_id, from_at)

folio_entries — Append-only money movements

folio_entries ------------- id UUID PK property_id UUID folio_id UUID FK business_date DATE kind TEXT amount_minor BIGINT reverses_id UUID NULL source_key TEXT Constraints: UNIQUE(property_id, source_key); no update policy Indexes: (property_id, folio_id, id); (property_id, business_date)

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

GET /v1/properties/:id/availability?from=&to= POST /v1/reservations POST /v1/stays/:id/check-in PATCH /v1/rooms/:id/housekeeping POST /v1/folios/:id/entries POST /v1/properties/:id/night-audit
EndpointPurpose and permissionIdempotency / status
GET /v1/properties/:id/availability?from=&to=Read room-type availability
Property member or public booking scope
Not required
200, 422
POST /v1/reservationsHold inventory and confirm booking
Booking client/front desk
Required
201, 409, 422
POST /v1/stays/:id/check-inAssign room and activate stay
Front desk
Transition id
200, 409
PATCH /v1/rooms/:id/housekeepingUpdate clean/inspected state
Assigned housekeeping or manager
Not required
200, 409
POST /v1/folios/:id/entriesPost charge, payment, or reversal
Front desk/finance by entry type
Required source key
201, 409, 422
POST /v1/properties/:id/night-auditAdvance 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

Hotel PMS system context
Hotel PMS system context

Main application architecture

Hotel PMS application architecture
Hotel PMS application architecture

Critical workflow sequence

Hotel PMS workflow sequence
Hotel PMS workflow sequence

Event model

EventProducer → consumersDelivery
ReservationConfirmedReservation module → Email, channel syncOutbox and dedupe by reservation/version
RoomStatusChangedHousekeeping → Front desk projectionVersioned last-write projection
BusinessDateAdvancedNight audit → Reports, integrationsReplayable; 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

LayerWhat it proves
UnitStay-date ranges, DST cases, folio reversal, state machines
IntegrationInventory locks, exclusion constraints, tenant policies, audit idempotency
ContractChannel and terminal message fixtures
ConcurrencyLast-room channel race and simultaneous room assignment
FailureAudit phase crash, offline terminal, late cancellation
End-to-endReserve → 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

Stage 1
Modular web app/API, PostgreSQL, queue worker, object storage
Stage 2
API replicas, integration workers, read replica for portfolio reports, Redis config cache
Stage 3
Property cohorts with independent operational databases; centralized read-only analytics

Scale evolution

These are architecture scenarios, not throughput claims. Each transition should follow measured workload and reliability pressure.

ScenarioArchitecture may evolve towardDecision rule
1K usersOne API, PostgreSQL with property-scoped rows, a channel-sync worker, and a scheduled night auditGuard each property's business date and room-night inventory; night audit runs exactly once per property and date
100K usersScale channel and reporting workers separately; route all property writes to one primary ownerSeparate channel-manager and reporting workers when OTA sync lag or report queries slow front-desk operations
1M+ usersPartition by property cohort/region; keep night audit and inventory single-writer per property; replicate sanitized events to analyticsMove property cohorts into separate cells for isolation or noisy-neighbour load; a property never spans cells