Skip to main content

inventory-service

Bounded Context: Inventory & Availability (Core) · Owner: PMS · Phase: 0 · Storage: Cloud SQL Postgres (shared schema + RLS, partitioned by month) + transactional outbox · Bundle: services/inventory-service/

inventory-service is the single source of truth for "is this room available right now?" on Ghasi Melmastoon. It owns the per-property × per-room-type × per-date availability calendar, the per-individual-room × per-date occupancy ledger, and the atomic allocation primitive that prevents overbooking. Every other service that needs to know whether a room can be booked, or that needs a room committed to a guest, talks to this service — directly via REST during search, and via the booking saga via Pub/Sub during commit.

The service does not own price (delegated to pricing-service), the reservation aggregate (reservation-service), housekeeping state (housekeeping-service), the physical room catalog (property-service), or key issuance (lock-integration-service). It receives property.room.created.v1 to extend its calendar, reacts to the reservation.* lifecycle to allocate or release, and publishes the events the booking saga and the meta-search aggregator depend on.

Purpose

  • Be the authoritative availability ledger per property × room-type × date and per individual room × date for every tenant.
  • Allocate rooms to reservations atomically with Postgres advisory locks; never let two saga branches commit to the same room-night.
  • Hold inventory under TTL when a guest is paying; release it the moment the saga aborts or the hold expires; commit it irrevocably the moment payment captures.
  • Block inventory for out-of-order (OOO) and out-of-service (OOS) events from property-service and propagate cascades that re-accommodate active reservations.
  • Reply to high-volume meta-search availability queries with low latency (read-optimized cache + authoritative fallback).
  • Replicate a 30-day rolling snapshot of property availability to the Electron desktop so the front desk can see and walk-in-allocate even when the cloud is unreachable, with a server-arbitration saga on reconnect.

Key responsibilities

  1. Availability queries — synchronous POST /availability/search for both meta-search (multi-property dates+occupancy) and tenant booking funnel (single-property calendar grid). Read path is hot.
  2. Atomic allocation — Postgres advisory lock keyed by (tenant_id, property_id, room_type_id, date) per night within stay; row-level FOR UPDATE on room_inventory_daily for assigned-room paths; commits a RoomAllocation aggregate.
  3. Hold lifecycle — tentative allocation with TTL during the booking saga; the hold timeout sweeper releases stranded holds every 30 s.
  4. Compensation — release allocation on reservation.cancelled.v1, reservation.dates_changed.v1 (release-then-reallocate), reservation.no_show.v1 (per policy), and on saga failure within reservation-service.
  5. Room assignment policy — auto-pick a specific room from the type pool (default; respects rotation and housekeeping hints) or honor a specific room hold (e.g., guest re-requests "the room with the mountain view").
  6. OOO / OOS blocks — first-class InventoryBlock aggregate; on creation, identifies overlapping active reservations and emits inventory.reaccommodation_required.v1 so reservation-service can run the room-change sub-saga.
  7. Group hold — atomic all-or-nothing multi-room hold; partial-fill is impossible by construction.
  8. Calendar extension — rolling 18-month horizon; daily job extends by one day per property; reacts to property.room.created.v1 to extend the new room's lane.
  9. Overbooking policy — per-tenant OverbookingPolicy aggregate (default off); when on, allows controlled oversell with explicit alert events; never silent.
  10. Mixed dorm/private inventory — guesthouses sell beds (dorms) and whole rooms; the service tracks both axes (bed_inventory_daily and room_inventory_daily) under a unified RoomTypeInventory umbrella.

Aggregates owned

AggregateCardinalityPurpose
AvailabilityCalendar1 per property × dateAggregate root for a single calendar day at a property; carries occupancy projections, sell-stop flags, blocks
RoomTypeInventory1 per property × room-type × datePer-room-type counters: total, held, committed, oos_blocked, available
RoomAllocation1 per reservation-itemThe committed room (or bed) for a stay-night-window; the only place we say "this is the assigned room"
InventoryBlock0..N per property × date rangeOOO / OOS / maintenance / event-block records; carries reason, source event id, expected duration
OverbookingPolicy0..1 per tenantPer-tenant oversell policy; default off; if on, declares cap, room-types covered, and alert routing

Key APIs (REST, /api/v1/inventory)

MethodPathPurpose
POST/availability/searchMulti-property availability for dates + occupancy (meta-search and funnel)
GET/properties/:propertyId/calendarFull month-grid calendar for backoffice
POST/allocationsPlace a tentative or committed allocation (saga step)
GET/allocations/:idRead an allocation
DELETE/allocations/:idRelease an allocation (compensation or cancel)
POST/blocksCreate an OOO/OOS block (manual or property-service-driven)
DELETE/blocks/:idRelease a block
GET/blocksList blocks (filter by property, date range, reason)
POST/groups/holdsAtomic multi-room hold for group bookings
GET/properties/:propertyId/policies/overbookingRead overbooking policy
PUT/properties/:propertyId/policies/overbookingUpdate overbooking policy (gm/owner)

Consumed by reservation-service (booking saga primary caller), bff-tenant-booking-service (funnel availability calls go via search-aggregation-service for cache, then this for confirm), bff-backoffice-service (calendar view, manual blocks), and search-aggregation-service (cache rebuilds).

Key events published

EventTrigger
melmastoon.inventory.availability.queried.v1Sampled (1% default) availability query for analytics
melmastoon.inventory.allocation.requested.v1A hold attempt entered the lock window
melmastoon.inventory.allocation.confirmed.v1Atomic allocation committed (saga forward path)
melmastoon.inventory.allocation.failed.v1Allocation rejected (no availability, lock timeout, policy)
melmastoon.inventory.allocation.released.v1Hold or commitment released (cancel, no-show, OOO cascade)
melmastoon.inventory.room.assigned.v1A specific room was assigned (auto-pick or explicit)
melmastoon.inventory.room.reassigned.v1A previously assigned room was changed (OOO cascade or guest request)
melmastoon.inventory.block.created.v1OOO/OOS block created
melmastoon.inventory.block.released.v1OOO/OOS block released
melmastoon.inventory.overbooking_alert.v1Oversell occurred under an active overbooking policy; on-call paged
melmastoon.inventory.reaccommodation_required.v1Active reservations affected by a new block — reservation-service runs room-change sub-saga

Key events consumed

EventEffect
melmastoon.reservation.held.v1Place tentative allocation with TTL
melmastoon.reservation.confirmed.v1Commit the tentative allocation; emit allocation.confirmed.v1
melmastoon.reservation.cancelled.v1Release allocation; emit allocation.released.v1
melmastoon.reservation.dates_changed.v1Release old, allocate new (sub-saga); emit room.reassigned.v1 if room changed
melmastoon.reservation.no_show.v1Release per tenant.no_show.policy (default release after grace)
melmastoon.reservation.hold_expired.v1Release the tentative allocation immediately
melmastoon.property.room.created.v1Extend calendar lane for the new room across the 18-month horizon
melmastoon.property.room.taken_out_of_order.v1Create InventoryBlock, emit reaccommodation_required.v1 for overlaps
melmastoon.property.room.returned_to_service.v1Release the OOO block; recompute availability
melmastoon.tenant.settings.changed.v1Refresh OverbookingPolicy, hold TTL defaults, calendar-horizon override

Upstream / downstream

Upstream (we consume): reservation-service, property-service, tenant-service, housekeeping-service (room-status hints for auto-pick).

Downstream (we publish for): reservation-service, search-aggregation-service (cache invalidation), analytics-service, audit-service, notification-service (overbooking alerts), sync-service (desktop snapshot pulls), bff-backoffice-service, bff-tenant-booking-service.

Non-functional requirements

NFRTarget
Allocation p99< 200 ms (advisory-lock window included)
Availability search p99< 300 ms (cold path; cached path < 50 ms via search-aggregation-service)
False-overbooking eventszero, ever — guarded by advisory locks + integration tests + Jepsen-style consistency check
Hold-expiry sweeper lag< 30 s beyond TTL
Calendar horizon18 months rolling, extended daily
Tenant isolationRLS-enforced; tenant-isolation.spec.ts mandatory in CI
Sync footprint30-day forward + 7-day backward property-availability snapshot to desktop SQLite
ReplicasMin 3 Cloud Run instances (hot read path) + separate hold-expiry sweeper + separate calendar-extender Cloud Run job

Where to go next