Low-Level Design: Travel Booking System — Flight Search, Seat Selection, and Itinerary Management

Core Entities

Flight: flight_id, airline, flight_number, origin_airport, destination_airport, departure_time, arrival_time, aircraft_type, status (SCHEDULED, BOARDING, DEPARTED, ARRIVED, CANCELLED, DELAYED). Seat: seat_id, flight_id, seat_number, class (ECONOMY, BUSINESS, FIRST), status (AVAILABLE, HELD, BOOKED), features (WINDOW, AISLE, EXTRA_LEGROOM). Booking: booking_id, booking_reference (6-char alphanumeric), user_id, status (CONFIRMED, CANCELLED, REFUNDED), total_price_cents, currency, created_at. BookingItem: item_id, booking_id, flight_id, passenger_id, seat_id, ticket_price_cents, fare_class. Passenger: passenger_id, first_name, last_name, passport_number, date_of_birth, nationality. Itinerary: itinerary_id, booking_id, legs (ordered list of flight_ids with connection times).

Flight Search

Search for flights between origin and destination on a given date. Direct flights: SELECT from flights WHERE origin=X AND destination=Y AND DATE(departure_time)=D AND status=SCHEDULED ORDER BY departure_time. Connecting flights: find one-stop connections. Query leg 1: origin=X, departure date=D. For each result: query leg 2 with origin=leg1.destination AND departure_time between leg1.arrival_time+60min AND leg1.arrival_time+360min (min/max connection window). This join can be expensive for large datasets. Pre-compute connections: a batch job builds a connections table for all city-pair combinations with valid layovers. Cache flight availability in Redis: available_seats:{flight_id} = SET of available seat IDs. Check SCARD (set cardinality) to show “7 seats left” without hitting the DB.

Seat Selection and Hold

When a user selects a seat during booking: create a temporary hold. Redis atomic operation: SREM available_seats:{flight_id} {seat_id} — returns 1 if the seat was in the set (available), 0 if already removed (unavailable). If 1: set a hold expiry key: SET seat_hold:{flight_id}:{seat_id} {session_id} EX 900 (15-minute hold). If 0: seat was taken concurrently — prompt user to select another. On payment success: remove from Redis available_seats (already removed), update DB seat status to BOOKED, delete the hold key. On session expiry (15 minutes without payment): a background job detects expired hold keys (Redis keyspace notifications on expiry events), releases the seat: SADD available_seats:{flight_id} {seat_id}, update DB status to AVAILABLE. This two-layer approach (Redis gate + DB source of truth) handles the high-concurrency seat selection at scale.

Pricing Engine

Flight prices are dynamic: they change based on demand, days until departure, seat class, and fare rules. Price components: base fare (set by the airline per fare class), taxes and fees (airport tax, fuel surcharge, carrier fees — varies by route), ancillary services (baggage, seat upgrade, meals). Fare classes: within Economy there are 10+ fare buckets (Y, B, M, H, K, L, V, Q, T, X) with different prices and restrictions (refundable, changeable, advance purchase required). On search: query current fares for each flight from a fare cache (updated every 5 minutes from the airline’s GDS connection). The displayed price = fare + all mandatory taxes. Fare lock: user can lock the current price for 24 hours for a small fee — store a PriceLock record with the locked amount and expiry.

Check-In and Boarding Pass

Online check-in opens 24-48 hours before departure. On check-in: verify booking is CONFIRMED and not cancelled. Collect travel documents (passport scan or number). Assign seat if not already selected (or confirm existing). Generate a boarding pass: boarding_pass_id, barcode (encodes: airline, flight number, passenger name, seat, booking reference, departure date/time). Store as PDF and send to the user’s email. Mobile boarding pass: generate a QR code or Aztec code. At the gate: scanner reads the barcode, calls the boarding API to validate: is this boarding pass for today’s flight? Has it already been scanned (prevent re-entry)? Mark as SCANNED in real time. Boarding sequence: sort by zone (first/business first, then economy by seat row).

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you prevent double-booking of the same airline seat during high-demand sales?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Two-layer approach identical to ticket booking. Layer 1 (Redis, fast gate): store available seat IDs in a Redis Set per flight: SADD available_seats:{flight_id} {seat_ids}. On selection: SREM available_seats:{flight_id} {seat_id}. SREM is atomic and returns 1 if removed (seat was available), 0 if already gone. If 0: seat is already taken — show user an error immediately. If 1: hold the seat in Redis with a TTL: SET seat_hold:{flight_id}:{seat_id} {session_id} EX 900. Layer 2 (Database): on payment success, UPDATE seats SET status=BOOKED WHERE seat_id=X AND status=AVAILABLE. If rows_affected==0: a race condition despite the Redis gate (Redis and DB briefly out of sync) — very rare but possible. Rollback, release the Redis hold, return an error. The Redis gate handles 99.999% of concurrency; the DB check is the authoritative fallback.”
}
},
{
“@type”: “Question”,
“name”: “How does a GDS (Global Distribution System) work in flight booking?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Airlines publish their flight inventory and fares to GDS providers (Amadeus, Sabre, Travelport). The GDS aggregates inventory from hundreds of airlines into a single system. Travel agencies and OTAs (online travel agencies like Expedia, Kayak) query the GDS to search flights and book tickets. GDS communication uses the EDIFACT or newer NDC (New Distribution Capability) protocol. On a flight search: the OTA sends a query to the GDS API. The GDS returns available flights with current availability (number of seats per fare class) and prices. On booking: the OTA sends a booking request to the GDS, which creates a PNR (Passenger Name Record) in the airline’s reservation system. The PNR is a unique record containing passenger details, flight, and seat. A copy is held at the GDS and at the airline. NDC allows airlines to sell directly to OTAs, bypassing the GDS for better margins.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle flight cancellations and refund processing at scale?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “When an airline cancels a flight: the airline sends a cancellation notification via GDS or direct API. The booking system receives the notification and: (1) Marks the flight status as CANCELLED. (2) Finds all BookingItems for this flight (can be thousands of bookings). (3) For each affected booking: evaluate the refund policy (non-refundable fare: travel credit only; refundable fare: full cash refund; airline-initiated cancellation: full refund regardless of fare type). (4) Create a Refund record for each booking. (5) Send notification emails/push to all affected passengers. (6) Queue refund processing via the payment processor (batch ACH or credit card reversal). Do this asynchronously via a Kafka topic — processing thousands of refunds synchronously would time out. Track refund status per booking. Offer rebooking on alternative flights before processing cash refunds — rebooking has higher margin than refund.”
}
},
{
“@type”: “Question”,
“name”: “How do you implement multi-leg itinerary search efficiently?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A multi-leg itinerary (e.g., NYC u2192 London u2192 Paris in one booking) is more complex than two independent searches. The connection time constraint varies by airport: minimum connection time at London Heathrow is 60 minutes (domestic to international), 90 minutes (international to international). Pre-compute valid connections: a nightly job builds a connections table: (origin, layover, destination, leg1_flight_id, leg2_flight_id, connection_minutes). Filter by connection validity: leg2.departure >= leg1.arrival + min_connection AND connection_minutes <= 360 (max 6-hour layover). Index on (origin, destination) for fast lookup. On search: query the connections table for all valid 1-stop itineraries between the origin and destination on the given date. For 2-stop itineraries: two joins against the connections table (expensive — cache common city-pair connection results). Total price = sum of individual leg prices. Availability: both legs must have available seats simultaneously."
}
},
{
"@type": "Question",
"name": "How do you implement dynamic seat pricing that updates in real time?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Airline seat pricing is a complex revenue management problem. Fare classes (Y, B, M, H, K, V, Q, T, X, L) represent availability buckets within economy. Higher fare classes are priced higher. As seats sell, the airline closes lower fare classes (no more $299 fares — only $499 and above remain). Implementation: the airline's revenue management system opens and closes fare class availability in near-real-time based on booking pace (how fast seats are selling vs. the forecast). This availability is published to the GDS every few minutes. The OTA caches the current fare class availability per flight (5-minute TTL). On search: retrieve cached availability. On booking attempt: re-verify availability with a live GDS check before confirming the price (the cached price may be stale). If the fare class closed between search and booking: present the next available price to the user and ask them to confirm the change."
}
}
]
}

Asked at: Airbnb Interview Guide

Asked at: Uber Interview Guide

Asked at: Stripe Interview Guide

Asked at: Lyft Interview Guide

Scroll to Top