Skip to main content

SERVICE_OVERVIEW — inventory-service

Sibling: DOMAIN_MODEL · APPLICATION_LOGIC · API_CONTRACTS · EVENT_SCHEMAS · DATA_MODEL

Strategic anchors: 02 Enterprise Architecture §6 Microservices Topology · 04 Event-Driven Architecture §7 Booking Saga · 03-microservices summary

1. Purpose

inventory-service is the authoritative availability ledger and atomic allocator for every property on Ghasi Melmastoon. Across all surfaces — meta-search consumer, tenant booking funnel, backoffice front desk, mobile, and the Electron desktop — when anyone asks "can this room be sold for this date?" or "give this guest this room for these nights," the answer comes from here. The service exists because availability has a single global truth at any instant and because the cost of getting it wrong (overbooking, double-allocation, stranded holds, refused valid bookings) is paid in trust, refunds, and operator time.

Allocation is a pure ledger operation with strict concurrency control. Pricing, payment, key issuance, housekeeping status — everything else about turning availability into a confirmed stay — is the responsibility of other services. We hold the calendar, the locks, and the rules; we publish the events; we never decide on price, money, or door state.

2. Bounded context

PropertyValue
Bounded contextInventory & Availability (Core)
DDD designationAggregate-rich, ledger-shaped, concurrency-critical
Owner teamPMS Core
Phase0 (ships with the platform's Day 1 booking saga)
Trust boundaryInternal; no public ingress
Scale shapeRead-heavy (search) with a moderate-volume, latency-sensitive write path (allocation)

The context boundary is sharp: anything that needs the count of rooms available or the assignment of a specific room to a specific stay belongs here. Anything that decides why a stay should happen (price, identity, payment, key, marketing) belongs to the service named for that responsibility.

3. Owned aggregates

AggregateID prefixCardinalityPurpose
AvailabilityCalendarcal_1 per property × dateDay-bucket aggregate root; carries day-level flags (stop_sell, partial OOO), summary counters, and a list of active blocks
RoomTypeInventoryrti_1 per property × room-type × dateThe hot row for search and allocation: total, held, committed, oos_blocked, available, plus per-bed counters for dorm types
RoomAllocationinv_1 per reservation-itemA committed (or held) assignment of a specific room (or bed) for a stay-night-window; the only place that names a physical room
InventoryBlockblk_0..N per property × date-rangeOOO/OOS/maintenance/event block; references a source.eventId for traceability
OverbookingPolicyobp_0..1 per tenantPer-tenant oversell rules; default off; carries cap, roomTypes[], alertRoutes[], effectiveFrom, effectiveUntil

The full type definitions live in DOMAIN_MODEL §2. Every aggregate is multi-tenant via tenantId and enforces an invariant that its tenant context cannot drift mid-transaction.

4. Service responsibilities (in scope)

  1. Hot-path availability search. Single endpoint accepts a list of property ids (or a meta-search filter), check-in/check-out dates, occupancy. Returns per-property per-room-type sellability with the granularity the caller asks for.
  2. Atomic allocation. Acquire a Postgres advisory lock keyed by (tenant_id, property_id, room_type_id, date) for every night in the stay. Re-validate availability under the lock. Write RoomAllocation + decrement counters + outbox the allocation.confirmed.v1 (or held.v1) event in a single transaction.
  3. Hold lifecycle. Tentative allocations carry a TTL provided by reservation-service (default 600 s, tenant-overridable). The hold-expiry sweeper releases stranded holds every 30 s.
  4. Compensation handlers for every reservation lifecycle event that affects allocation.
  5. OOO/OOS block management. Create blocks from operator action or property.room.taken_out_of_order.v1; identify overlapping reservations; emit reaccommodation_required.v1.
  6. Auto-pick room from type pool vs. specific-room hold policy at allocation time.
  7. Group atomic hold. All rooms or none; never partial-fill.
  8. Calendar horizon maintenance. A daily job extends every property's calendar by one day; on property.room.created.v1, extend the new room across the entire horizon.
  9. Per-tenant overbooking policy with explicit, auditable, alert-emitting oversell.
  10. Desktop pull endpoint for the 30-day-forward / 7-day-back property-availability snapshot.

5. Out of scope (explicit non-responsibilities)

  • Pricing of any kind. Including dynamic price floors, rate-plan availability, length-of-stay pricing, MLOS/MaxLOS rules. → pricing-service and rate-plan-service.
  • The reservation aggregate itself. Including identity of the guest, money, payment, lifecycle past confirmed. → reservation-service.
  • Housekeeping state. A room being clean/dirty/inspected does not change its availability for new bookings; we consume housekeeping hints only to bias the auto-pick selector. → housekeeping-service.
  • Physical room metadata. Room number, floor, capacity, amenities, vendor lock device id. → property-service.
  • Key/door state. Whether the lock can be opened or not. → lock-integration-service.
  • Search ranking and filtering for the meta layer. That's a denormalized projection in search-aggregation-service; we feed it via events.

6. Service-to-service interactions

┌──────────────────────────────┐
│ bff-backoffice-service │
└─────────────┬────────────────┘
│ REST: blocks, calendar grid

┌───────────────────┐ ┌──────────────────────┐ ┌────────────────────────┐
│ reservation-svc │◀──▶│ inventory-service │◀──▶│ property-service │
│ (saga primary) │ │ (this service) │ │ (rooms, OOO/OOS) │
└─────────┬─────────┘ └──────────┬───────────┘ └────────────────────────┘
│ events: held / confirmed │ events: allocation.* / block.*
│ cancelled / dates_changed │ reaccommodation_required.v1
▼ ▼
┌────────────────┐ ┌──────────────────────────┐
│ payment-gw-svc │ │ search-aggregation-svc │
└────────────────┘ │ (cache, meta search) │
└──────────────────────────┘

Synchronous REST is reserved for availability search (read) and manual operator flows (block create, calendar grid). Allocation and release flow through the booking saga via events; no synchronous call from reservation-service to inventory-service for allocation in steady state — only on the walk-in fast-path where the WalkInAllocateUseCase opens a server-side mini-saga.

7. Booking saga role

The booking saga is documented in 04 Event-Driven Architecture §7. inventory-service's role:

reservation.held.v1


inventory-service
─ acquire advisory locks for every night
─ validate availability under lock
─ write RoomAllocation (status='held')
─ emit inventory.allocation.confirmed.v1 (or .failed.v1)


reservation-service advances to payment
... payment.transaction.captured.v1 ...


reservation.confirmed.v1


inventory-service
─ flip RoomAllocation to status='committed'
─ emit inventory.allocation.confirmed.v1 (idempotent re-emit on commit)
─ trigger room.assigned.v1 if a specific room was picked under lock

Compensation paths (see FAILURE_MODES) all funnel into inventory.allocation.released.v1 with a typed reasonCode.

8. Hot-path constraints

ConstraintHow it's met
Allocation p99 < 200 msAdvisory lock acquisition + single transactional batch; no synchronous outbound RPC; outbox publish is async; index-tight queries
Availability search p99 < 300 ms (cold)Per-property room_type_inventory_daily index by (tenant_id, property_id, room_type_id, stay_date); partition pruning by month; search-aggregation-service carries a Memorystore cache with sub-50 ms hit path
Zero false-overbookingThree layers: advisory locks; row-level CHECK constraint available >= 0; EXCLUDE constraint on room_allocations for any single physical room across overlapping nights (see DATA_MODEL §3.4)
Hold-expiry < 30 s lagSingle-replica Cloud Run sweeper triggered by Cloud Scheduler every 30 s; idempotent batch with FOR UPDATE SKIP LOCKED
Calendar always extendsCloud Scheduler nightly extend-calendar-horizon job; alert if a property is < 30 days short of horizon

9. Multi-tenancy and isolation

Every aggregate carries tenant_id. Every table has RLS policy <table>_tenant_isolation (DATA_MODEL §3). Every advisory lock key is hashtextextended(tenant_id::text || ':' || property_id || ':' || room_type_id || ':' || stay_date::text, 0)::bigint, so locks are also tenant-scoped at the database layer — even a misrouted query cannot deadlock against another tenant's allocation.

10. Offline / Electron desktop

The desktop pulls a property-scoped 30-day-forward + 7-day-back snapshot. It can perform walk-in allocations offline under a local pending_offline_allocation flag. On reconnect, sync-service pushes pending allocations; the server arbitrates: if the room is still free, the allocation commits and a held reservation is bumped to pending; if the room was taken, the offline allocation is rejected and the front-desk operator is prompted to reassign. See SYNC_CONTRACT.

11. NFR summary

NFRTargetSource of truth
Allocation p99 latency< 200 msOBSERVABILITY §3
Availability search p99< 300 ms (cold), < 50 ms (cached)OBSERVABILITY §3
False overbooking events0 in any 30-day windowTESTING_STRATEGY §4 + §7
Hold-expiry sweeper lag< 30 s p99OBSERVABILITY §3
Outbox lag< 5 s p99OBSERVABILITY §3
Calendar horizon≥ 540 days (18 mo) per active propertyAPPLICATION_LOGIC §5
API availability99.95% monthlyDEPLOYMENT_TOPOLOGY §5
Tenant isolationRLS + advisory-lock-key + integration testSECURITY_MODEL §3

12. Cross-references