How to Build an Online Booking System: Architecture and Features Guide
Booking systems look simple on the surface - pick a date, pick a time, confirm. Under the hood, they involve race conditions, timezone nightmares, cancellation logic, and notification chains. Here's how to build one that actually works.
The Core Problem: Preventing Double-Bookings
This is the hardest part of any booking system. Two users click "Book 3:00 PM" at the same time. You must ensure only one succeeds.
Wrong approach: Check availability → show as available → save booking. The gap between check and save creates a race condition.
Correct approach: Database-level locking + transactions.
-- PostgreSQL: Atomic slot reservation
BEGIN;
SELECT id FROM slots
WHERE start_time = '2026-04-10 15:00:00'
AND staff_id = 5
AND status = 'available'
FOR UPDATE SKIP LOCKED
LIMIT 1;
-- If row found, update it
UPDATE slots SET status = 'booked', booking_id = $1
WHERE id = $selected_id;
COMMIT;
FOR UPDATE SKIP LOCKED acquires a row-level lock. Concurrent transactions skip locked rows rather than waiting. No double-booking possible.
Database Schema (Core Tables)
-- Resources being booked (staff, rooms, equipment)
resources (
id, name, type, business_id, timezone, is_active
)
-- Availability windows
availability (
id, resource_id, day_of_week, start_time, end_time
)
-- Booked slots
bookings (
id, resource_id, customer_id,
start_time TIMESTAMPTZ, end_time TIMESTAMPTZ,
status ENUM('pending', 'confirmed', 'cancelled', 'completed', 'no_show'),
notes, created_at, cancelled_at, cancellation_reason
)
-- Customers
customers (
id, name, email, phone, business_id, notes
)
-- Services (what's being booked)
services (
id, business_id, name, duration_minutes, price, buffer_time_minutes
)
Store all times as TIMESTAMPTZ (timestamp with timezone). Never store local times - you will regret it.
Feature List by Business Type
Universal (All Booking Systems)
- Calendar view (day/week/month)
- Available slot display
- Booking confirmation (email + SMS)
- Cancellation with configurable notice window
- Reschedule
- Reminder notifications (24h + 1h before)
- Admin panel to manage bookings
- Customer booking history
Hair Salon / Barbershop / Beauty
- Staff selection
- Service duration + buffer time between appointments
- Multiple services in one booking
- Client notes (e.g., "allergic to X")
- Recurring booking (same time every week)
Medical / Dental Clinic
- Doctor/specialist selection
- Appointment type (consultation, procedure, follow-up)
- Patient intake form (pre-appointment questionnaire)
- Medical history notes
- Insurance information fields
- HIPAA/data protection compliance
Restaurant
- Table size selection
- Deposit / prepayment option
- Special requests (birthday, dietary restrictions)
- Waitlist for fully booked slots
- Auto-release tables if not checked in within 15 min
Hotel / Accommodation
- Date range selection (check-in / check-out)
- Room type availability matrix
- Rate calculation (price varies by date, room type)
- OTA integration (Booking.com, Airbnb)
Calendar Integration
Most businesses need two-way sync with Google Calendar or Outlook:
// Google Calendar API example
const { google } = require('googleapis');
const calendar = google.calendar('v3');
// Create event when booking is confirmed
await calendar.events.insert({
calendarId: 'primary',
resource: {
summary: `Appointment: ${customerName}`,
start: { dateTime: booking.startTime },
end: { dateTime: booking.endTime },
description: booking.notes,
}
});
This keeps staff calendars in sync automatically. Block manual calendar entries from accepting bookings (sync goes both ways).
Timezone Handling
The most common source of bugs in booking systems:
- Store all times in UTC in the database
- Convert to local time only for display
- Never store "3:00 PM" - always store
2026-04-10T09:00:00Zwith explicit UTC offset
// Use date-fns-tz for reliable timezone conversion
import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz';
const userLocalTime = '2026-04-10 15:00'; // User sees this
const timezone = 'Asia/Bishkek'; // UTC+6
const utcTime = zonedTimeToUtc(userLocalTime, timezone); // Store this
Notification System
Bookings require multiple notification triggers:
| Event | Notification |
|---|---|
| Booking created | Confirmation email + SMS to customer |
| Booking created | Alert to staff/admin |
| 24h before | Reminder to customer |
| 1h before | Final reminder |
| Cancellation | Email to customer with reason |
| No-show | Internal flag, optional customer email |
Use a queue (BullMQ, Inngest) for scheduled notifications. Don't run setTimeout in your API - it won't survive server restarts.
Payment Integration
Options for requiring deposits or full prepayment:
Stripe: Best for international, cards, SEPA
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'usd',
product_data: { name: serviceName },
unit_amount: depositAmount * 100,
},
quantity: 1,
}],
mode: 'payment',
success_url: `${domain}/booking/confirm?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${domain}/booking/cancel`,
});
Booking is only confirmed after checkout.session.completed webhook fires.
Build vs Buy
| Solution | Cost | Fit |
|---|---|---|
| Calendly / Cal.com | $12–50/month | Simple appointments, no customization |
| Booksy / Treatwell | Revenue % or $50/month | Beauty industry specific |
| Custom built | $15,000–50,000 | Full control, matches your exact workflow |
| White-label platform | $5,000–20,000 | Faster than custom, some limitations |
Build custom when: existing solutions don't match your workflow, you need integration with your specific CRM/POS, or you're building a platform for multiple businesses.