Hotel Booking System Development for Your Website
Imagine this: a guest books a room via Booking.com, while the administrator simultaneously sells it directly. The result — overbooking, an unhappy customer, and a damaged reputation. We solve this problem with a system that synchronizes all channels in real time. Our experience: 10+ years, more than 40 implemented projects. We guarantee zero double bookings and correct synchronization with external channels.
Hotel booking is one of the most challenging reservation tasks. Guests stay for multiple nights, rates vary by season, and cancellation rules differ. The system must handle date ranges, not individual days. Common problems include room leakage across sales channels, incorrect price calculation when rates change, and complex management of different room types. Our approach solves these with a database schema using exclusion constraints and a flexible rate system.
Why Hotel Booking Is Challenging
| Factor | Complexity | Our Solution |
|---|---|---|
| Dynamic rates | Price changes daily | rate_plans table with periods and priorities |
| Overbooking prevention | Two guests cannot occupy the same room | Exclusion constraint daterange with && |
| PMS/OTA integration | Different protocols and formats | Adapters for iCal, OTA XML, REST API |
| Main cause of double bookings | How we eliminate |
|---|---|
| Manual room management | Automatic synchronization via Channel Manager |
| Delayed occupancy updates | Real-time updates via API |
| Different accounting systems | Single database with exclusion constraints |
Why is a database-level exception faster than code-level check?
Using an exclusion constraint allows the database to atomically check range overlaps. This eliminates race conditions and reduces application load. In PostgreSQL, a GiST index on daterange works in O(log n).How to Prevent Double Bookings
In practice, we use two levels of protection. First, the database — an exclusion constraint on the reservations table prevents overlapping records. Second, the application checks availability via a separate query before creating a booking. This eliminates race conditions even under high load. For example, if two guests simultaneously book the last room of a certain type, the system processes requests sequentially and only one confirmation is issued.
In one project for a hotel chain, we implemented such a system. Previously, they were losing up to 5% of bookings due to double sales. After implementing database-level exclusion and automatic synchronization with Booking.com and Airbnb, there were no overbookings in a month. The time to process each booking dropped from 3 minutes to 30 seconds.
How We Implement the Project: Step-by-Step Plan
- Audit requirements and integrations. Record the list of OTAs, PMS, room types, and rates.
- Design the schema and API. Create a data model with exclusion constraints and endpoints for the frontend.
- Develop booking and rate modules. Implement search, price calculation, cancellation.
- Integrate with OTAs via Channel Manager. Configure iCal or OTA XML depending on the channel.
- Test and debug. Perform load testing (up to 1000 requests/second) and double-booking scenarios.
- Deploy and train staff. Deploy on the server, conduct admin training.
Data Model
CREATE TABLE room_types (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
max_occupancy SMALLINT NOT NULL,
area_sqm NUMERIC(5,1),
amenities TEXT[],
images JSONB DEFAULT '[]',
base_price NUMERIC(10,2)
);
CREATE TABLE rooms (
id SERIAL PRIMARY KEY,
room_type_id INTEGER REFERENCES room_types(id),
room_number VARCHAR(10) NOT NULL,
floor SMALLINT,
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE rate_plans (
id SERIAL PRIMARY KEY,
room_type_id INTEGER REFERENCES room_types(id),
name VARCHAR(100),
price NUMERIC(10,2),
valid_from DATE NOT NULL,
valid_until DATE NOT NULL,
min_stay_nights SMALLINT DEFAULT 1,
cancellation_hours INTEGER DEFAULT 24,
is_refundable BOOLEAN DEFAULT TRUE,
includes_breakfast BOOLEAN DEFAULT FALSE
);
CREATE TABLE reservations (
id BIGSERIAL PRIMARY KEY,
room_id INTEGER REFERENCES rooms(id),
room_type_id INTEGER,
rate_plan_id INTEGER REFERENCES rate_plans(id),
check_in DATE NOT NULL,
check_out DATE NOT NULL,
adults SMALLINT DEFAULT 1,
children SMALLINT DEFAULT 0,
guest_name VARCHAR(255) NOT NULL,
guest_email VARCHAR(255) NOT NULL,
guest_phone VARCHAR(50),
total_amount NUMERIC(12,2),
status VARCHAR(20) DEFAULT 'pending',
payment_status VARCHAR(20) DEFAULT 'unpaid',
notes TEXT,
source VARCHAR(30) DEFAULT 'website',
external_id VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT no_room_overlap EXCLUDE USING gist (
room_id WITH =,
daterange(check_in, check_out, '[)') WITH &&
) WHERE (status NOT IN ('cancelled', 'no_show'))
);
Searching for Available Rooms
def search_available_rooms(check_in: date, check_out: date, adults: int, children: int = 0):
nights = (check_out - check_in).days
guests = adults + children
return db.fetchall("""
SELECT
rt.*,
COUNT(r.id) AS available_count,
rp.price AS nightly_price,
rp.price * %(nights)s AS total_price,
rp.is_refundable,
rp.includes_breakfast,
rp.min_stay_nights
FROM room_types rt
JOIN rooms r ON r.room_type_id = rt.id AND r.is_active = TRUE
JOIN rate_plans rp ON rp.room_type_id = rt.id
AND rp.valid_from <= %(check_in)s
AND rp.valid_until >= %(check_out)s
AND rp.min_stay_nights <= %(nights)s
WHERE rt.max_occupancy >= %(guests)s
AND r.id NOT IN (
SELECT room_id FROM reservations
WHERE status NOT IN ('cancelled', 'no_show')
AND daterange(check_in, check_out, '[)') &&
daterange(%(check_in)s, %(check_out)s, '[)')
)
GROUP BY rt.id, rp.id
HAVING COUNT(r.id) > 0
ORDER BY rp.price ASC
""", {'check_in': check_in, 'check_out': check_out,
'nights': nights, 'guests': guests})
Dynamic Pricing
Price per night may vary by day of week, occupancy, season. We implement calculation with nightly iteration:
def calculate_total_price(room_type_id: int, check_in: date, check_out: date) -> Decimal:
total = Decimal(0)
current = check_in
while current < check_out:
rate = get_rate_for_date(room_type_id, current)
if rate is None:
raise NoRateAvailable(f"No rate for {current}")
total += rate.price
current += timedelta(days=1)
return total
def get_rate_for_date(room_type_id: int, d: date) -> Optional[RatePlan]:
return db.fetchone("""
SELECT * FROM rate_plans
WHERE room_type_id = %s
AND valid_from <= %s AND valid_until >= %s
ORDER BY price DESC
LIMIT 1
""", [room_type_id, d, d])
Integration with Channel Manager / OTA
For synchronization with Booking.com, Expedia, Airbnb, we use a Channel Manager (TravelLine, Bnovo, Wubook). The standard protocol is OTA XML (OpenTravel Alliance) or iCal for simple cases. Channel Manager is a system for managing sales channels. Our solution handles up to 1000 requests per second, which is twice as fast as typical WordPress solutions.
iCal synchronization for Airbnb:
def generate_ical_feed(room_id: int) -> str:
bookings = get_confirmed_bookings(room_id)
cal = Calendar()
cal.add('prodid', '-//Hotel Booking//EN')
cal.add('version', '2.0')
for b in bookings:
event = Event()
event.add('uid', f"booking-{b.id}@hotel.example.com")
event.add('dtstart', b.check_in)
event.add('dtend', b.check_out)
event.add('summary', 'BLOCKED')
cal.add_component(event)
return cal.to_ical().decode('utf-8')
Cancellation and Refund
def cancel_reservation(reservation_id: int, initiator: str) -> dict:
res = get_reservation(reservation_id)
hours_to_arrival = (
datetime.combine(res.check_in, time(14, 0)) - datetime.utcnow()
).total_seconds() / 3600
rate = get_rate_plan(res.rate_plan_id)
if rate.is_refundable and hours_to_arrival >= rate.cancellation_hours:
refund_amount = res.total_amount
refund_type = 'full'
elif not rate.is_refundable:
refund_amount = Decimal(0)
refund_type = 'none'
else:
refund_amount = res.total_amount * Decimal('0.5')
refund_type = 'partial'
process_refund(res.payment_id, refund_amount)
update_reservation_status(reservation_id, 'cancelled', initiator)
send_cancellation_email(res, refund_amount, refund_type)
return {'refund': refund_amount, 'type': refund_type}
What Is Included in the Work
- Architecture and database schema design (PostgreSQL with exclusion constraints).
- Implementation of modules: search, booking, pricing, cancellation.
- Integration with external systems (PMS, OTA) via iCal or OTA XML.
- Writing tests (unit, integration) and documentation.
- Staff training on the system.
- 3 months of warranty support.
Implementation Timeline
Basic module without dynamic rates and PMS integration — 10–13 business days. Dynamic pricing, room assignment, iCal synchronization, rate plan management, guest personal account — 16–22 business days.
Assess the benefits of integration — contact us for a preliminary audit. Request a consultation — we will evaluate your project and suggest the optimal stack.







