Implementing Restaurant Table Booking on a Website
Table booking is a task with several non-trivial details: table capacity must match guest count, restaurant operates in shifts (lunch/dinner), and overbooking is sometimes desired behavior.
Hall and Table Model
CREATE TABLE restaurant_halls (
id SERIAL PRIMARY KEY,
name VARCHAR(100), -- 'Main Hall', 'Terrace', 'VIP'
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE restaurant_tables (
id SERIAL PRIMARY KEY,
hall_id INTEGER REFERENCES restaurant_halls(id),
table_number VARCHAR(10),
min_guests SMALLINT DEFAULT 1,
max_guests SMALLINT NOT NULL,
is_combinable BOOLEAN DEFAULT FALSE, -- can move adjacent
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE table_bookings (
id BIGSERIAL PRIMARY KEY,
table_id INTEGER REFERENCES restaurant_tables(id),
guest_name VARCHAR(255) NOT NULL,
guest_phone VARCHAR(50) NOT NULL,
guest_email VARCHAR(255),
guests_count SMALLINT NOT NULL,
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
special_wishes TEXT,
deposit_paid BOOLEAN DEFAULT FALSE,
deposit_amount NUMERIC(10,2),
source VARCHAR(30) DEFAULT 'website', -- 'website','phone','walkin'
created_at TIMESTAMP DEFAULT NOW()
);
Shifts and Time Slots
Restaurant doesn't book a table "at 15:37" — only for a shift or fixed slot:
SHIFTS = {
'lunch': {'start': time(12, 0), 'end': time(15, 0), 'duration': timedelta(hours=2)},
'dinner': {'start': time(18, 0), 'end': time(23, 0), 'duration': timedelta(hours=2)},
}
def get_booking_slots(date: date) -> list[dict]:
slots = []
for shift_name, shift in SHIFTS.items():
current = datetime.combine(date, shift['start'])
end_dt = datetime.combine(date, shift['end'])
while current + shift['duration'] <= end_dt:
slots.append({
'shift': shift_name,
'starts_at': current,
'ends_at': current + shift['duration'],
})
current += timedelta(hours=1) # slots every hour
return slots
Finding Table by Guest Count
def find_available_table(guests: int, starts_at: datetime, ends_at: datetime, hall_id: int = None):
query = """
SELECT t.*
FROM restaurant_tables t
WHERE t.min_guests <= %(guests)s
AND t.max_guests >= %(guests)s
AND t.is_active = TRUE
AND (%(hall_id)s IS NULL OR t.hall_id = %(hall_id)s)
AND t.id NOT IN (
SELECT table_id FROM table_bookings
WHERE status NOT IN ('cancelled', 'no_show')
AND tsrange(starts_at, ends_at, '[)') && tsrange(%(starts)s, %(ends)s, '[)')
)
ORDER BY t.max_guests ASC -- select smallest suitable table
LIMIT 1
"""
return db.fetchone(query, {
'guests': guests, 'hall_id': hall_id,
'starts': starts_at, 'ends': ends_at,
})
Combining Tables
If there's no 8-person table but two adjacent 4-person tables exist — can suggest combining. Requires an adjacency table:
CREATE TABLE table_adjacency (
table_a INTEGER REFERENCES restaurant_tables(id),
table_b INTEGER REFERENCES restaurant_tables(id),
PRIMARY KEY (table_a, table_b)
);
Confirmation and Reminders
| Time | Action |
|---|---|
| Immediately | SMS/email to client with booking details |
| 24 hours before | SMS reminder |
| 2 hours before | SMS "We're waiting for you at 19:00" |
| +30 min after start | If no-show — change status to no_show, release table |
| +1 day | Email requesting review |
Optional deposit when booking — payment gateway integration. Deposit is charged if guest doesn't show and hasn't cancelled N hours before.
Implementation Timeline
Basic system with one hall, shifts and SMS/email notifications — 6–8 business days. Multiple halls, table combining, deposits, CMS management, booking history — 10–13 business days.







