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
| Property | Value |
|---|---|
| Bounded context | Inventory & Availability (Core) |
| DDD designation | Aggregate-rich, ledger-shaped, concurrency-critical |
| Owner team | PMS Core |
| Phase | 0 (ships with the platform's Day 1 booking saga) |
| Trust boundary | Internal; no public ingress |
| Scale shape | Read-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
| Aggregate | ID prefix | Cardinality | Purpose |
|---|---|---|---|
AvailabilityCalendar | cal_ | 1 per property × date | Day-bucket aggregate root; carries day-level flags (stop_sell, partial OOO), summary counters, and a list of active blocks |
RoomTypeInventory | rti_ | 1 per property × room-type × date | The hot row for search and allocation: total, held, committed, oos_blocked, available, plus per-bed counters for dorm types |
RoomAllocation | inv_ | 1 per reservation-item | A committed (or held) assignment of a specific room (or bed) for a stay-night-window; the only place that names a physical room |
InventoryBlock | blk_ | 0..N per property × date-range | OOO/OOS/maintenance/event block; references a source.eventId for traceability |
OverbookingPolicy | obp_ | 0..1 per tenant | Per-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)
- 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.
- 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. WriteRoomAllocation+ decrement counters + outbox theallocation.confirmed.v1(orheld.v1) event in a single transaction. - 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. - Compensation handlers for every reservation lifecycle event that affects allocation.
- OOO/OOS block management. Create blocks from operator action or
property.room.taken_out_of_order.v1; identify overlapping reservations; emitreaccommodation_required.v1. - Auto-pick room from type pool vs. specific-room hold policy at allocation time.
- Group atomic hold. All rooms or none; never partial-fill.
- 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. - Per-tenant overbooking policy with explicit, auditable, alert-emitting oversell.
- 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-serviceandrate-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
| Constraint | How it's met |
|---|---|
| Allocation p99 < 200 ms | Advisory 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-overbooking | Three 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 lag | Single-replica Cloud Run sweeper triggered by Cloud Scheduler every 30 s; idempotent batch with FOR UPDATE SKIP LOCKED |
| Calendar always extends | Cloud 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
| NFR | Target | Source of truth |
|---|---|---|
| Allocation p99 latency | < 200 ms | OBSERVABILITY §3 |
| Availability search p99 | < 300 ms (cold), < 50 ms (cached) | OBSERVABILITY §3 |
| False overbooking events | 0 in any 30-day window | TESTING_STRATEGY §4 + §7 |
| Hold-expiry sweeper lag | < 30 s p99 | OBSERVABILITY §3 |
| Outbox lag | < 5 s p99 | OBSERVABILITY §3 |
| Calendar horizon | ≥ 540 days (18 mo) per active property | APPLICATION_LOGIC §5 |
| API availability | 99.95% monthly | DEPLOYMENT_TOPOLOGY §5 |
| Tenant isolation | RLS + advisory-lock-key + integration test | SECURITY_MODEL §3 |
12. Cross-references
- Domain types and invariants: DOMAIN_MODEL
- Use cases and saga participation: APPLICATION_LOGIC
- REST surface: API_CONTRACTS
- Event surfaces: EVENT_SCHEMAS
- DDL, partitioning, advisory-lock function: DATA_MODEL
- Desktop replication: SYNC_CONTRACT
- Reservation counterpart: reservation-service SERVICE_OVERVIEW
- Saga walk-through: 04 §7