Hospitality / travel
Hospitality PMS & channel manager
A property management system and channel manager that keeps availability, rates and reservations in sync across OTAs and GDS — where a single mismatch means an overbooking.
Overview
Properties that sell rooms don’t sell them in one place. The same room shows up on Booking.com, Airbnb, Expedia, a couple of GDS channels and the hotel’s own site — and every one of those wants to be told, immediately, when a room is booked or a price changes. This project was the system in the middle: a property management system (PMS) and channel manager that treats all of those surfaces as one inventory.
Problem
The failure mode here is not subtle. If availability drifts between two channels — even for a few seconds under load — you either sell the same room twice (an overbooking, and an angry guest at the desk) or you hide a room that was actually free (lost revenue). Rates have the same problem in a quieter form: a stale price on one channel undercuts every other.
Doing this by hand, or with a nightly batch job, is not good enough. The window where the systems disagree is the bug. The real requirement was two-way sync — we both push our truth out and accept updates coming back — with the drift window kept as small as we could make it.
Solution
A PMS and channel manager with two-way synchronization of availability, rates and reservations. It integrates with third-party PMS and channel-management systems, the major OTAs, and GDS distribution, and presents them to the property as a single source of truth. When a reservation lands on any channel, the inventory it consumed is reflected everywhere else before the next channel can sell it.
Architecture
The core is a Laravel service that owns the canonical inventory and rate model, with a Vue.js operator UI on top. The interesting part is the integration layer: each channel speaks a different dialect, at a different cadence, with different ideas about what “the same room” means, so every one sits behind an adapter that normalizes into our internal events.
- Inbound channel updates and outbound pushes are decoupled through queues (Redis), so a slow or flaky partner API never blocks the operator UI or the rest of the fan-out.
- Reservation and availability changes are modelled as events, which makes the two-way flow reason-about-able: an update is either ours going out or theirs coming in, and the reconciliation rules live in one place.
- The whole thing runs in Docker, which mattered more than usual here — reproducing a partner’s edge-case payload locally is how most of these bugs actually get fixed.
Technical challenges
The hard problems were all about consistency under concurrency. Two channels confirming a booking for the last room within the same second is a genuine race, and “just lock everything” isn’t an option when a lock held during a third-party HTTP call can stall the whole property. Getting the boundaries right — what must be strongly consistent (the last unit of inventory) versus what can settle a moment later (a rate nudge) — was most of the work.
Partner APIs are also a source of endless, undocumented surprise: rate limits that aren’t published, retries that silently double-post, fields that mean subtly different things per channel. A lot of the resilience is defensive plumbing around other people’s systems.
Lessons learned
Model the domain as events before you touch a single integration. Once “availability changed” and “reservation received” were first-class things in our system rather than side effects of an API call, every new channel became a mapping exercise instead of a rewrite. And decide early, explicitly, which invariants are worth blocking for — most aren’t, and pretending they all are is how you build something that’s correct and unusably slow.