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-serviceand 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
- Availability queries — synchronous
POST /availability/searchfor both meta-search (multi-property dates+occupancy) and tenant booking funnel (single-property calendar grid). Read path is hot. - Atomic allocation — Postgres advisory lock keyed by
(tenant_id, property_id, room_type_id, date)per night within stay; row-levelFOR UPDATEonroom_inventory_dailyfor assigned-room paths; commits aRoomAllocationaggregate. - Hold lifecycle — tentative allocation with TTL during the booking saga; the hold timeout sweeper releases stranded holds every 30 s.
- Compensation — release allocation on
reservation.cancelled.v1,reservation.dates_changed.v1(release-then-reallocate),reservation.no_show.v1(per policy), and on saga failure withinreservation-service. - 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").
- OOO / OOS blocks — first-class
InventoryBlockaggregate; on creation, identifies overlapping active reservations and emitsinventory.reaccommodation_required.v1soreservation-servicecan run the room-change sub-saga. - Group hold — atomic all-or-nothing multi-room hold; partial-fill is impossible by construction.
- Calendar extension — rolling 18-month horizon; daily job extends by one day per property; reacts to
property.room.created.v1to extend the new room's lane. - Overbooking policy — per-tenant
OverbookingPolicyaggregate (default off); when on, allows controlled oversell with explicit alert events; never silent. - Mixed dorm/private inventory — guesthouses sell beds (dorms) and whole rooms; the service tracks both axes (
bed_inventory_dailyandroom_inventory_daily) under a unifiedRoomTypeInventoryumbrella.
Aggregates owned
| Aggregate | Cardinality | Purpose |
|---|---|---|
AvailabilityCalendar | 1 per property × date | Aggregate root for a single calendar day at a property; carries occupancy projections, sell-stop flags, blocks |
RoomTypeInventory | 1 per property × room-type × date | Per-room-type counters: total, held, committed, oos_blocked, available |
RoomAllocation | 1 per reservation-item | The committed room (or bed) for a stay-night-window; the only place we say "this is the assigned room" |
InventoryBlock | 0..N per property × date range | OOO / OOS / maintenance / event-block records; carries reason, source event id, expected duration |
OverbookingPolicy | 0..1 per tenant | Per-tenant oversell policy; default off; if on, declares cap, room-types covered, and alert routing |
Key APIs (REST, /api/v1/inventory)
| Method | Path | Purpose |
|---|---|---|
POST | /availability/search | Multi-property availability for dates + occupancy (meta-search and funnel) |
GET | /properties/:propertyId/calendar | Full month-grid calendar for backoffice |
POST | /allocations | Place a tentative or committed allocation (saga step) |
GET | /allocations/:id | Read an allocation |
DELETE | /allocations/:id | Release an allocation (compensation or cancel) |
POST | /blocks | Create an OOO/OOS block (manual or property-service-driven) |
DELETE | /blocks/:id | Release a block |
GET | /blocks | List blocks (filter by property, date range, reason) |
POST | /groups/holds | Atomic multi-room hold for group bookings |
GET | /properties/:propertyId/policies/overbooking | Read overbooking policy |
PUT | /properties/:propertyId/policies/overbooking | Update 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
| Event | Trigger |
|---|---|
melmastoon.inventory.availability.queried.v1 | Sampled (1% default) availability query for analytics |
melmastoon.inventory.allocation.requested.v1 | A hold attempt entered the lock window |
melmastoon.inventory.allocation.confirmed.v1 | Atomic allocation committed (saga forward path) |
melmastoon.inventory.allocation.failed.v1 | Allocation rejected (no availability, lock timeout, policy) |
melmastoon.inventory.allocation.released.v1 | Hold or commitment released (cancel, no-show, OOO cascade) |
melmastoon.inventory.room.assigned.v1 | A specific room was assigned (auto-pick or explicit) |
melmastoon.inventory.room.reassigned.v1 | A previously assigned room was changed (OOO cascade or guest request) |
melmastoon.inventory.block.created.v1 | OOO/OOS block created |
melmastoon.inventory.block.released.v1 | OOO/OOS block released |
melmastoon.inventory.overbooking_alert.v1 | Oversell occurred under an active overbooking policy; on-call paged |
melmastoon.inventory.reaccommodation_required.v1 | Active reservations affected by a new block — reservation-service runs room-change sub-saga |
Key events consumed
| Event | Effect |
|---|---|
melmastoon.reservation.held.v1 | Place tentative allocation with TTL |
melmastoon.reservation.confirmed.v1 | Commit the tentative allocation; emit allocation.confirmed.v1 |
melmastoon.reservation.cancelled.v1 | Release allocation; emit allocation.released.v1 |
melmastoon.reservation.dates_changed.v1 | Release old, allocate new (sub-saga); emit room.reassigned.v1 if room changed |
melmastoon.reservation.no_show.v1 | Release per tenant.no_show.policy (default release after grace) |
melmastoon.reservation.hold_expired.v1 | Release the tentative allocation immediately |
melmastoon.property.room.created.v1 | Extend calendar lane for the new room across the 18-month horizon |
melmastoon.property.room.taken_out_of_order.v1 | Create InventoryBlock, emit reaccommodation_required.v1 for overlaps |
melmastoon.property.room.returned_to_service.v1 | Release the OOO block; recompute availability |
melmastoon.tenant.settings.changed.v1 | Refresh 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
| NFR | Target |
|---|---|
| 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 events | zero, ever — guarded by advisory locks + integration tests + Jepsen-style consistency check |
| Hold-expiry sweeper lag | < 30 s beyond TTL |
| Calendar horizon | 18 months rolling, extended daily |
| Tenant isolation | RLS-enforced; tenant-isolation.spec.ts mandatory in CI |
| Sync footprint | 30-day forward + 7-day backward property-availability snapshot to desktop SQLite |
| Replicas | Min 3 Cloud Run instances (hot read path) + separate hold-expiry sweeper + separate calendar-extender Cloud Run job |
Where to go next
- Implementation-grade detail:
services/inventory-service/SERVICE_OVERVIEW.mdand the rest of the 17-doc bundle. - Booking saga participation:
docs/04-event-driven-architecture.md§7. - Conflict policy for the desktop sync engine (
server_authoritativefor inventory):docs/02-enterprise-architecture.md§8.2. - Overbooking philosophy and tenant policy surface:
services/inventory-service/SECURITY_MODEL.md§authorization for policy edits. - Reservation orchestration counterpart:
docs/03-microservices/reservation-service.md.