system-design55 min read

Design a Travel Booking System: The Complete Guide — A Senior+ Guide | Ayodhyya

Design a Travel Booking System: The Complete Guide

Building Expedia, Booking.com, and MakeMyTrip at scale — flights, hotels, cars, pricing engines, and multi-currency payments

Senior+ Guide 50+ min read 10,000+ words Ayodhyya

Table of Contents

  1. Introduction — The Travel Booking Landscape
  2. Functional & Non-Functional Requirements
  3. Capacity Estimation & Back-of-Envelope
  4. Data Model & Storage Schema
  5. High-Level Architecture
  6. API Design
  7. Search & Filtering Engine
  8. Flight, Hotel & Car Inventory
  9. Dynamic Pricing Engine
  10. Booking Flow & State Machine
  11. Payment & Refunds
  12. Itinerary Management
  13. Review & Rating System
  14. Travel Alerts & Notifications
  15. Multi-Currency & Localization
  16. Loyalty Program
  17. Partner API Integration
  18. Cancellation & Rebooking
  19. Fraud Detection
  20. Monitoring & Observability
  21. Cost Estimation
  22. Testing Strategy
  23. Interview Q&A

1. Introduction — The Travel Booking Landscape

The global online travel market is projected to exceed $1.1 trillion by 2027. Platforms like Booking.com serve over 28 million accommodation listings across 220 countries. Expedia processes more than 200 million room nights per year. MakeMyTrip, one of India's largest travel portals, handles millions of flight and hotel bookings daily. At first glance a travel booking system looks straightforward — search for flights or hotels, compare prices, and click "Book Now." Beneath the surface, however, it is one of the most intricate distributed systems in modern software engineering.

A production travel platform must aggregate inventory from dozens of Global Distribution Systems (GDS) such as Amadeus, Sabre, and Travelport, each with its own XML or JSON API and wildly different data shapes. It must run a dynamic pricing engine that recalculates fares based on demand signals, competitor prices, seasonality, and user behaviour in real time. It must enforce seat or room allocation with strict concurrency control so that two users never book the same seat on the same flight. It must support dozens of currencies, local payment methods, and regulatory compliance requirements such as PSD2 in Europe and GST in India. It must also handle the entire lifecycle of a booking — from search through payment confirmation, ticket issuance, itinerary changes, cancellations, refunds, and rebookings.

Interview Context: The travel booking system design question is a staple at Amazon (Alexa Shopping), Booking.com, Expedia, Uber Travel, and Airbnb. It tests your knowledge of inventory management, distributed transactions, rate-limiting third-party APIs, optimistic locking, saga patterns, and event-driven architectures. A senior engineer is expected to go beyond basic CRUD and discuss seat-inventory allocation with hold timers, price-lock mechanisms, and the challenges of multi-source aggregation.

This guide walks you through every layer of the system. We will cover functional and non-functional requirements, back-of-the-envelope capacity estimation, a detailed relational and NoSQL data model, the high-level microservice architecture, API design for search and booking, the search and filtering pipeline with Elasticsearch, inventory management for flights, hotels, and cars, a dynamic pricing engine with surge and discount logic, a booking state machine with saga-based distributed transactions, payment processing with PCI-DSS considerations, itinerary management, review and rating systems, real-time travel alerts, multi-currency and localization, loyalty programs, partner API integration, cancellation and rebooking workflows, fraud detection, monitoring and observability, cost estimation, and testing strategies. The article concludes with a comprehensive interview Q&A section.

Why This Problem Is Hard

The fundamental difficulty is the combination of real-time inventory from external suppliers, price volatility, and the requirement for strong consistency in booking. Unlike an e-commerce system where you can oversell and backorder, selling a seat that does not exist is an unrecoverable error. You must hold inventory atomically — lock a seat for a customer while they enter payment details, then either confirm or release within a strict timeout. This hold-and-confirm pattern, combined with dozens of GDS integrations each with different latency profiles and error semantics, creates a system that is far more complex than a typical CRUD application.

Additionally, the read-to-write ratio is extreme. For every booking, there are thousands of searches. This means the read path must be highly optimised with caching, CDN-delivered search results, and denormalised data, while the write path must be strongly consistent with distributed locking and idempotency guarantees.

Consider the scale of data involved: a single airline partner might expose 500,000 flight segments daily across 10,000 routes. A hotel aggregator might index 5 million room-nights across 300,000 properties. When you multiply these across multiple GDS partners, each with their own response format, rate limits, and availability semantics, the aggregation challenge becomes clear. You need a robust adapter layer that normalises all of this into a unified internal model while gracefully handling partner-specific quirks and outages.

The competitive landscape adds another dimension of complexity. Users expect real-time prices, instant confirmation, and a seamless cross-device experience. They compare your prices against Google Flights, Skyscanner, and direct airline websites within seconds. Your search must be blazingly fast, your prices must be competitive, and your booking flow must be frictionless. Every millisecond of latency in search results translates directly to lost conversions.

2. Functional & Non-Functional Requirements

Functional Requirements

#RequirementPriorityDetails
F1Flight search and bookingMustMulti-city, round-trip, one-way with filters
F2Hotel search and bookingMustDate range, location, star rating, amenities
F3Car rental search and bookingMustPickup/dropoff location, vehicle type, insurance
F4Dynamic pricingMustReal-time fare updates based on demand and inventory
F5Payment processingMustCredit cards, digital wallets, bank transfers, EMI
F6Booking managementMustView, cancel, modify, reprint e-tickets
F7User accounts and profilesMustRegistration, login, saved travellers, preferences
F8Multi-currency supportShouldDisplay and pay in local currency
F9Review and ratingShouldUsers rate hotels, flights, and cars
F10Loyalty programShouldPoints accrual, redemption, tier status
F11Travel alertsShouldFlight delay, gate change, weather disruptions
F12Cancellation and refundMustFull, partial, non-refundable fare rules
F13RebookingShouldDate or route changes with fare difference
F14Partner APIShouldWhite-label API for OTAs and affiliates
F15Fraud detectionMustBlock stolen cards, velocity checks, device fingerprinting

Non-Functional Requirements

RequirementTargetRationale
Search latency (P95)< 500msUsers expect near-instant search results
Booking latency (P95)< 3 secondsEnd-to-end from confirm to confirmation page
Availability99.99%Downtime during peak travel seasons costs millions
Data consistencyStrong for bookingsOverselling a seat or room is unacceptable
Read:Write ratio1000:1Searches vastly outnumber bookings
Throughput50,000 QPS (search), 500 QPS (booking)Peak season demand
Data retention7 yearsRegulatory and tax compliance
SecurityPCI-DSS Level 1Payment card handling

The non-functional requirements shape every architectural decision. The 500ms search latency target demands that we cache aggressively and precompute popular routes. The 99.99% availability target requires multi-AZ deployment with automated failover. The strong consistency requirement for bookings means we cannot use eventual consistency for inventory allocation — we need distributed locks or serializable transactions. The 1000:1 read-write ratio informs our strategy of separating the read path (search) from the write path (booking) with different consistency and scaling profiles.

3. Capacity Estimation & Back-of-Envelope

Traffic Estimates

Assume 10 million daily active users. Each user performs an average of 5 searches per session with a session duration of 20 minutes. This yields 50 million searches per day or roughly 600 searches per second. With a peak factor of 10x during holiday seasons, peak search QPS reaches 6,000. Assuming a 1% conversion rate from search to booking, we get 500,000 bookings per day or about 6 bookings per second, peaking at 60. The revenue per booking averages $450, giving us a daily gross merchandise value of $225 million and an annual GMV exceeding $82 billion.

Storage Estimates

EntitySize per RecordDaily VolumeAnnual Storage
Search logs2 KB50 million~36 TB/year
Bookings5 KB500K~9 GB/year
User profiles1 KBSteady state~4 GB (10M users)
Hotel listings4 KBSteady state~112 GB (28M listings)
Reviews1.5 KB50K~27 GB/year
Payment transactions3 KB500K~5.5 GB/year

Bandwidth Estimates

Search results averaging 50 KB each at 6,000 QPS produce approximately 300 MB/s of outbound data. Search result pages should be cached at the CDN edge, reducing origin bandwidth to roughly 10% or 30 MB/s. Inbound traffic from GDS partners averages 50 MB/s when polling for availability updates. WebSocket connections for real-time alerts consume roughly 5 MB/s for 100,000 concurrent users with heartbeat messages every 30 seconds.

Cache Estimates

Hot search results for popular routes (e.g., New York to London) and destinations (e.g., Paris hotels in December) can be cached for 60 seconds, covering approximately 80% of search traffic with roughly 50 GB of Redis cache. Hotel static content (images, descriptions) can be CDN-cached for 24 hours, consuming approximately 2 TB of CDN storage. User session data and price locks require another 10 GB of Redis with TTL-based eviction.

public class CapacityCalculator
{
    public static void Estimate()
    {
        var dailyActiveUsers = 10_000_000L;
        var searchesPerSession = 5;
        var dailySearches = dailyActiveUsers * searchesPerSession; // 50M
        var searchQps = dailySearches / 86_400; // ~579
        var peakSearchQps = searchQps * 10; // ~5,790
        var conversionRate = 0.01;
        var dailyBookings = dailySearches * conversionRate; // 500K
        var bookingQps = dailyBookings / 86_400; // ~5.8
        var peakBookingQps = bookingQps * 10; // ~58
        var avgBookingValue = 450m;
        var dailyGMV = dailyBookings * avgBookingValue; // $225M
        var annualGMV = dailyGMV * 365; // ~$82B
        
        Console.WriteLine($"Search QPS: {searchQps}, Peak: {peakSearchQps}");
        Console.WriteLine($"Booking QPS: {bookingQps:F1}, Peak: {peakBookingQps:F1}");
        Console.WriteLine($"Daily GMV: ${dailyGMV:N0}, Annual: ${annualGMV:N0}");
        
        // Storage
        var searchLogStoragePerYear = dailySearches * 2048L * 365; // bytes
        Console.WriteLine($"Search logs/year: {searchLogStoragePerYear / (1024*1024*1024*1024.0):F1} TB");
    }
}
Key Insight: The 1000:1 read-to-write ratio means your architecture must be read-optimised. Use CDN, Redis caching, and read replicas aggressively on the search path while reserving strong consistency guarantees (distributed locks, two-phase commits) only for the booking write path.

4. Data Model & Storage Schema

The data model must handle structured relational data (users, bookings, payments), semi-structured inventory data from multiple suppliers, and high-volume search analytics. We use PostgreSQL as the primary transactional store, Elasticsearch for search indexing, Redis for caching and session holds, and Apache Kafka for event streaming. The data model is designed around the concept of a Booking containing one or more BookingLegs, where each leg represents a single flight segment, hotel stay, or car rental period.

Entity Relationship Overview

erDiagram USER ||--o{ BOOKING : makes USER ||--o{ REVIEW : writes USER ||--o{ LOYALTY_ACCOUNT : has BOOKING ||--|| PAYMENT : has BOOKING ||--o{ BOOKING_LEG : contains BOOKING ||--o| CANCELLATION : may_have BOOKING_LEG ||--|| FLIGHT_INVENTORY : references BOOKING_LEG ||--|| HOTEL_ROOM : references BOOKING_LEG ||--|| CAR_RENTAL : references FLIGHT_INVENTORY }|--|| AIRPORT : from FLIGHT_INVENTORY }|--|| AIRPORT : to HOTEL_ROOM }|--|| HOTEL : belongs_to HOTEL }|--|| CITY : located_in CAR_RENTAL }|--|| CAR_SUPPLIER : from

Core Tables

CREATE TABLE users (
    user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    full_name VARCHAR(200) NOT NULL,
    phone VARCHAR(20),
    country_code VARCHAR(3),
    preferred_currency VARCHAR(3) DEFAULT 'USD',
    preferred_language VARCHAR(5) DEFAULT 'en',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    is_verified BOOLEAN DEFAULT FALSE,
    is_blocked BOOLEAN DEFAULT FALSE
);

CREATE TABLE bookings (
    booking_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES users(user_id),
    booking_ref VARCHAR(12) UNIQUE NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    total_amount DECIMAL(12,2) NOT NULL,
    currency VARCHAR(3) NOT NULL DEFAULT 'USD',
    payment_status VARCHAR(20) DEFAULT 'UNPAID',
    booked_at TIMESTAMPTZ,
    expires_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    metadata JSONB
);

CREATE TABLE booking_legs (
    leg_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    booking_id UUID REFERENCES bookings(booking_id),
    leg_type VARCHAR(10) NOT NULL,
    supplier_id VARCHAR(50),
    supplier_confirmation VARCHAR(50),
    origin_code VARCHAR(10),
    destination_code VARCHAR(10),
    departure_time TIMESTAMPTZ,
    arrival_time TIMESTAMPTZ,
    passenger_count INT DEFAULT 1,
    fare_class VARCHAR(20),
    base_fare DECIMAL(12,2),
    taxes DECIMAL(12,2),
    fees DECIMAL(12,2),
    total_fare DECIMAL(12,2),
    status VARCHAR(20) DEFAULT 'CONFIRMED',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE payments (
    payment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    booking_id UUID REFERENCES bookings(booking_id),
    amount DECIMAL(12,2) NOT NULL,
    currency VARCHAR(3) NOT NULL,
    method VARCHAR(30) NOT NULL,
    gateway VARCHAR(30) NOT NULL,
    gateway_txn_id VARCHAR(100),
    status VARCHAR(20) DEFAULT 'PENDING',
    card_last_four VARCHAR(4),
    card_brand VARCHAR(20),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    processed_at TIMESTAMPTZ,
    refund_amount DECIMAL(12,2) DEFAULT 0,
    metadata JSONB
);

CREATE TABLE inventory_locks (
    lock_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    inventory_type VARCHAR(20) NOT NULL,
    inventory_id VARCHAR(100) NOT NULL,
    user_id UUID REFERENCES users(user_id),
    locked_at TIMESTAMPTZ DEFAULT NOW(),
    expires_at TIMESTAMPTZ NOT NULL,
    session_id VARCHAR(100),
    UNIQUE(inventory_type, inventory_id)
);

CREATE TABLE reviews (
    review_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES users(user_id),
    entity_type VARCHAR(20) NOT NULL,
    entity_id UUID NOT NULL,
    rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
    title VARCHAR(200),
    body TEXT,
    is_verified_booking BOOLEAN DEFAULT FALSE,
    helpful_count INT DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE loyalty_accounts (
    account_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES users(user_id) UNIQUE,
    points_balance BIGINT DEFAULT 0,
    tier VARCHAR(20) DEFAULT 'BRONZE',
    lifetime_points BIGINT DEFAULT 0,
    points_expiry_date DATE,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE price_locks (
    lock_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id VARCHAR(100) NOT NULL,
    inventory_type VARCHAR(20) NOT NULL,
    inventory_id VARCHAR(100) NOT NULL,
    locked_price DECIMAL(12,2) NOT NULL,
    currency VARCHAR(3) NOT NULL,
    locked_at TIMESTAMPTZ DEFAULT NOW(),
    expires_at TIMESTAMPTZ NOT NULL,
    is_used BOOLEAN DEFAULT FALSE
);

NoSQL Collections for Search Analytics

Search queries, clickstream events, and pricing history are written to Kafka and consumed into ClickHouse for analytical queries. Elasticsearch stores denormalised flight, hotel, and car inventory for full-text search with faceted filtering. Redis stores price locks, session data, and frequently-accessed search result caches. The separation of transactional (PostgreSQL) and analytical (ClickHouse) data stores allows each to be optimised independently — PostgreSQL for ACID transactions with B-tree indexes, and ClickHouse for columnar analytical queries with aggressive compression.

StorePurposeRetentionConsistency
PostgreSQLUsers, bookings, payments, reviews7 yearsStrong (ACID)
ElasticsearchInventory search, autocompleteReal-time syncEventual
RedisPrice locks, search cache, sessionsTTL-basedEventual
ClickHouseSearch analytics, pricing history3 yearsEventual
KafkaEvent streaming, CDC from PostgreSQL30 daysAt-least-once

Database Sharding Strategy

PostgreSQL is sharded by user_id using consistent hashing. This ensures that all bookings for a single user land on the same shard, enabling efficient joins and transactional consistency within a user's scope. The bookings table uses a composite index on (user_id, created_at DESC) for fast retrieval of recent bookings. Inventory data is partitioned by region (Americas, Europe, Asia-Pacific) to keep queries local and reduce cross-region latency. Elasticsearch indices are sharded by origin airport code to distribute search load evenly across nodes.

5. High-Level Architecture

The system is decomposed into domain-aligned microservices: Search Service, Inventory Service, Pricing Service, Booking Service, Payment Service, User Service, Notification Service, and Partner Gateway. Each service owns its own data store and communicates through synchronous REST/gRPC for queries and asynchronous Kafka events for commands and state changes. An API Gateway handles authentication, rate limiting, and request routing. A Service Mesh (Istio or Linkerd) provides mTLS, circuit breaking, and observability.

flowchart TB subgraph Clients["Client Applications"] Web["Web App"] Mobile["Mobile App"] PartnerAPI["Partner API"] end subgraph Gateway["API Gateway"] AuthN["Authentication"] RateLimit["Rate Limiter"] Router["Request Router"] end subgraph CoreServices["Core Microservices"] SearchSvc["Search Service"] InventorySvc["Inventory Service"] PricingSvc["Pricing Service"] BookingSvc["Booking Service"] PaymentSvc["Payment Service"] UserSvc["User Service"] NotifSvc["Notification Service"] end subgraph DataLayer["Data Stores"] PG[(PostgreSQL)] ES[(Elasticsearch)] Redis[(Redis)] Kafka["Kafka"] CH[(ClickHouse)] end subgraph External["External Partners"] Amadeus["Amadeus GDS"] Sabre["Sabre GDS"] Stripe["Payment Gateway"] Twilio["SMS/Push"] end Web --> Gateway Mobile --> Gateway PartnerAPI --> Gateway Gateway --> SearchSvc Gateway --> BookingSvc Gateway --> UserSvc SearchSvc --> ES SearchSvc --> Redis InventorySvc --> Amadeus InventorySvc --> Sabre InventorySvc --> PG PricingSvc --> Redis PricingSvc --> Kafka BookingSvc --> PG BookingSvc --> Redis BookingSvc --> Kafka PaymentSvc --> Stripe PaymentSvc --> PG UserSvc --> PG NotifSvc --> Twilio NotifSvc --> Kafka Kafka --> CH

Service Responsibilities

ServiceResponsibilityData StoreCommunication
Search ServiceFull-text search, filtering, autocomplete, geo-searchElasticsearch + Redis cacheSynchronous (gRPC)
Inventory ServiceAggregate inventory from GDS partners, cache availabilityPostgreSQL + RedisAsync (Kafka) + Sync (gRPC)
Pricing ServiceDynamic pricing, surge calculation, discount applicationRedis + ClickHouseAsync (Kafka events)
Booking ServiceBooking creation, state machine, saga orchestrationPostgreSQLSaga + Kafka events
Payment ServicePayment capture, refunds, reconciliationPostgreSQLSynchronous (REST to gateway)
User ServiceAuthentication, profiles, preferences, loyaltyPostgreSQLSynchronous (gRPC)
Notification ServiceEmail, SMS, push, in-app notificationsEvent-driven (Kafka)Async (Kafka consumers)
Partner GatewayAdapter layer for GDS/airline/hotel APIsPostgreSQL (config)Async (Kafka producers)
Design Principle: The Inventory Service acts as a facade over multiple GDS adapters (Amadeus, Sabre, Travelport, direct airline/hotel APIs). Each adapter normalises supplier-specific XML/JSON responses into a common inventory model. This means adding a new supplier requires only a new adapter — no changes to the Search, Pricing, or Booking services. This adapter pattern follows the Strangler Fig migration strategy, allowing us to incrementally replace legacy integrations without downtime.

Event-Driven Communication

Cross-service communication uses an event-driven architecture with Apache Kafka as the central message bus. Key event streams include: booking.events (state changes in the booking lifecycle), payment.events (payment confirmations and failures), inventory.events (availability updates from GDS partners), pricing.events (price change notifications), and notification.events (alerts to be dispatched). Each event is stamped with a unique ID, timestamp, and version number to support schema evolution. Consumers implement idempotent processing with idempotency keys to handle at-least-once delivery guarantees.

The event schema is managed through a Schema Registry (Confluent or Apicurio) with backward and forward compatibility modes. This ensures that producers can evolve event schemas without breaking existing consumers. For example, when we add a new field to the BookingConfirmedEvent (such as a baggage allowance), older consumers simply ignore the unknown field.

6. API Design

Search API

// POST /api/v1/search/flights
[ApiController]
[Route("api/v1/search")]
public class SearchController : ControllerBase
{
    private readonly ISearchService _searchService;

    [HttpPost("flights")]
    public async Task<ActionResult<FlightSearchResponse>> SearchFlights(
        [FromBody] FlightSearchRequest request)
    {
        var results = await _searchService.SearchFlightsAsync(
            origin: request.OriginCode,
            destination: request.DestinationCode,
            departureDate: request.DepartureDate,
            returnDate: request.ReturnDate,
            passengers: request.PassengerCounts,
            cabinClass: request.CabinClass,
            currency: request.Currency ?? "USD",
            maxStops: request.MaxStops,
            sortBy: request.SortBy ?? "price",
            page: request.Page,
            pageSize: Math.Min(request.PageSize, 50));

        return Ok(results);
    }

    [HttpPost("hotels")]
    public async Task<ActionResult<HotelSearchResponse>> SearchHotels(
        [FromBody] HotelSearchRequest request)
    {
        var results = await _searchService.SearchHotelsAsync(
            cityCode: request.CityCode,
            checkIn: request.CheckInDate,
            checkOut: request.CheckOutDate,
            rooms: request.RoomConfigs,
            starRatings: request.StarRatings,
            priceRange: request.PriceRange,
            amenities: request.Amenities,
            sortBy: request.SortBy ?? "price",
            page: request.Page);

        return Ok(results);
    }

    [HttpPost("cars")]
    public async Task<ActionResult<CarSearchResponse>> SearchCars(
        [FromBody] CarSearchRequest request)
    {
        var results = await _searchService.SearchCarsAsync(
            pickupLocation: request.PickupLocationCode,
            dropoffLocation: request.DropoffLocationCode ?? request.PickupLocationCode,
            pickupTime: request.PickupDateTime,
            dropoffTime: request.DropoffDateTime,
            vehicleType: request.VehicleType,
            transmission: request.Transmission,
            supplierFilter: request.PreferredSuppliers);

        return Ok(results);
    }
}

Booking API

// POST /api/v1/bookings
[ApiController]
[Route("api/v1/bookings")]
[Authorize]
public class BookingController : ControllerBase
{
    private readonly IBookingService _bookingService;

    [HttpPost]
    public async Task<ActionResult<BookingResponse>> CreateBooking(
        [FromBody] CreateBookingRequest request)
    {
        var booking = await _bookingService.CreateBookingAsync(
            userId: GetUserId(),
            sessionId: request.SessionId,
            priceLockId: request.PriceLockId,
            legs: request.Legs,
            passengers: request.Passengers,
            contactInfo: request.ContactInfo,
            specialRequests: request.SpecialRequests);

        return CreatedAtAction(
            nameof(GetBooking),
            new { bookingId = booking.BookingId },
            booking);
    }

    [HttpGet("{bookingId}")]
    public async Task<ActionResult<BookingDetailResponse>> GetBooking(
        Guid bookingId)
    {
        var booking = await _bookingService.GetBookingAsync(bookingId, GetUserId());
        return Ok(booking);
    }

    [HttpPost("{bookingId}/cancel")]
    public async Task<ActionResult<CancelResponse>> CancelBooking(
        Guid bookingId, [FromBody] CancelRequest request)
    {
        var result = await _bookingService.CancelBookingAsync(
            bookingId, GetUserId(), request.Reason);
        return Ok(result);
    }

    [HttpPost("{bookingId}/rebook")]
    public async Task<ActionResult<RebookResponse>> RebookBooking(
        Guid bookingId, [FromBody] RebookRequest request)
    {
        var result = await _bookingService.RebookAsync(
            bookingId, GetUserId(), request.NewDates, request.NewLegs);
        return Ok(result);
    }
}

Price Lock API

// POST /api/v1/price-locks
[ApiController]
[Route("api/v1/price-locks")]
public class PriceLockController : ControllerBase
{
    private readonly IPricingService _pricingService;

    [HttpPost]
    public async Task<ActionResult<PriceLockResponse>> LockPrice(
        [FromBody] PriceLockRequest request)
    {
        var lockResult = await _pricingService.LockPriceAsync(
            sessionId: request.SessionId,
            inventoryType: request.InventoryType,
            inventoryId: request.InventoryId,
            ttlSeconds: 600);

        return Ok(lockResult);
    }

    [HttpGet("{lockId}")]
    public async Task<ActionResult<PriceLockStatus>> GetLockStatus(Guid lockId)
    {
        var status = await _pricingService.GetLockStatusAsync(lockId);
        return Ok(status);
    }
}

API Endpoints Summary

MethodEndpointDescriptionAuth
POST/api/v1/search/flightsSearch flightsOptional
POST/api/v1/search/hotelsSearch hotelsOptional
POST/api/v1/search/carsSearch rental carsOptional
POST/api/v1/price-locksLock a price for 10 minRequired
POST/api/v1/bookingsCreate a bookingRequired
GET/api/v1/bookings/{id}Get booking detailsRequired
POST/api/v1/bookings/{id}/cancelCancel a bookingRequired
POST/api/v1/bookings/{id}/rebookRebook with new datesRequired
POST/api/v1/paymentsProcess paymentRequired
POST/api/v1/payments/{id}/refundInitiate refundRequired
GET/api/v1/users/{id}/loyaltyGet loyalty balanceRequired
POST/api/v1/reviewsSubmit a reviewRequired

All API endpoints are versioned under /api/v1/ to allow backward-compatible evolution. Responses follow a consistent envelope format with data, errors, and meta fields. Pagination uses cursor-based pagination for search results (returning a next_cursor token) and offset-based pagination for list endpoints. Rate limits are applied per API key with separate quotas for authenticated and unauthenticated requests.

7. Search & Filtering Engine

The search engine is the most traffic-intensive component. It must deliver sub-second results across millions of inventory items while supporting complex filters (price range, stops, airline, departure time, duration, cabin class, hotel amenities, car type). We use Elasticsearch as the primary search index with Redis caching for hot queries and a write-behind pipeline that synchronises inventory changes from PostgreSQL and GDS feeds into the index.

Elasticsearch Index Schema

public class FlightIndexDocument
{
    public string Id { get; set; }
    public string OriginCode { get; set; }
    public string OriginCity { get; set; }
    public string OriginCountry { get; set; }
    public string DestinationCode { get; set; }
    public string DestinationCity { get; set; }
    public string DestinationCountry { get; set; }
    public DateTime DepartureTime { get; set; }
    public DateTime ArrivalTime { get; set; }
    public int DurationMinutes { get; set; }
    public int StopCount { get; set; }
    public List<string> StopAirports { get; set; }
    public string AirlineCode { get; set; }
    public string AirlineName { get; set; }
    public string AircraftType { get; set; }
    public decimal BasePrice { get; set; }
    public string Currency { get; set; }
    public Dictionary<string, decimal> PriceByCurrency { get; set; }
    public int AvailableSeats { get; set; }
    public string CabinClass { get; set; }
    public bool IsRefundable { get; set; }
    public bool IncludesMeals { get; set; }
    public int BaggageAllowanceKg { get; set; }
    public GeoPoint OriginLocation { get; set; }
    public GeoPoint DestinationLocation { get; set; }
    public DateTime LastUpdated { get; set; }
}

public class HotelIndexDocument
{
    public string Id { get; set; }
    public string HotelName { get; set; }
    public string CityCode { get; set; }
    public string CityName { get; set; }
    public string CountryCode { get; set; }
    public GeoPoint Location { get; set; }
    public int StarRating { get; set; }
    public decimal PricePerNight { get; set; }
    public string Currency { get; set; }
    public Dictionary<string, decimal> PriceByDate { get; set; }
    public List<string> Amenities { get; set; }
    public decimal ReviewScore { get; set; }
    public int ReviewCount { get; set; }
    public List<string> ImageUrls { get; set; }
    public bool FreeCancellation { get; set; }
    public bool BreakfastIncluded { get; set; }
    public int AvailableRooms { get; set; }
    public DateTime LastUpdated { get; set; }
}

Search Caching Strategy

Search results are cached in Redis with a composite cache key derived from origin, destination, dates, cabin class, and currency. Hot routes like JFK-LAX or DEL-BOM are precomputed and cached. Cache TTL is 60 seconds for price data (to keep fares reasonably current) and 10 minutes for static hotel data. A stale-while-revalidate pattern ensures the UI shows results immediately even if the cache is expired, with a background refresh updating the cached copy.

public class CachedSearchService : ISearchService
{
    private readonly ISearchService _inner;
    private readonly IDistributedCache _cache;
    private readonly ILogger<CachedSearchService> _logger;

    public async Task<FlightSearchResult> SearchFlightsAsync(FlightSearchRequest req)
    {
        var cacheKey = BuildCacheKey("flights", req);
        var cached = await _cache.GetAsync<FlightSearchResult>(cacheKey);
        
        if (cached != null)
        {
            if (!IsStale(cached, TimeSpan.FromSeconds(60)))
                return cached.Result;

            // Stale-while-revalidate: return stale, refresh in background
            _ = Task.Run(async () =>
            {
                var fresh = await _inner.SearchFlightsAsync(req);
                await _cache.SetAsync(cacheKey, new CacheEntry(fresh, TimeSpan.FromSeconds(60)));
            });
            return cached.Result;
        }

        var result = await _inner.SearchFlightsAsync(req);
        await _cache.SetAsync(cacheKey, new CacheEntry(result, TimeSpan.FromSeconds(60)));
        return result;
    }

    private string BuildCacheKey(string type, FlightSearchRequest req)
    {
        var parts = new[] {
            type, req.OriginCode, req.DestinationCode,
            req.DepartureDate.ToString("yyyyMMdd"),
            req.ReturnDate?.ToString("yyyyMMdd") ?? "OW",
            req.CabinClass?.ToString() ?? "ECONOMY",
            req.Currency ?? "USD"
        };
        return $"search:{string.Join(":", parts)}";
    }

    private bool IsStale(CacheEntry entry, TimeSpan threshold)
    {
        return DateTimeOffset.UtcNow - entry.CachedAt > threshold;
    }
}

Search Performance Optimisations

TechniqueImpactImplementation
Redis result caching80% cache hit rate60s TTL, stale-while-revalidate
CDN edge caching60% origin offloadCache-Control headers on search pages
Elasticsearch shardingParallel search3 shards per index, 1 replica
Prefilter by price range70% candidate reductionNumeric range query before text search
Completion suggester< 10ms autocompletePrefix-based city/airport suggestions
Geo-distance filteringFast proximity searchGeo_point field with distance sort

Search Result Ranking

Default sorting is by "best value" which combines price, duration, number of stops, and airline rating into a composite score. Users can override this with explicit sort options: cheapest first, fastest first, earliest departure, or best rated. The ranking algorithm assigns weights: price (40%), duration (25%), stops (20%), and airline rating (15%). These weights are tuned via A/B testing to maximise click-through rate and booking conversion.

8. Flight, Hotel & Car Inventory

Inventory management is the beating heart of a travel platform. Unlike e-commerce where you have full control over your stock, travel inventory lives outside your system — in airline reservation systems, hotel property management systems, and car rental company databases. You must poll, cache, and synchronise this external inventory while respecting rate limits and data freshness requirements.

Flight Inventory Architecture

sequenceDiagram participant User participant SearchSvc as Search Service participant Cache as Redis Cache participant InvSvc as Inventory Service participant Amadeus as Amadeus GDS participant Sabre as Sabre GDS User->>SearchSvc: Search flights DEL to BOM, 2026-08-15 SearchSvc->>Cache: Check cache alt Cache hit Cache-->>SearchSvc: Cached results else Cache miss SearchSvc->>InvSvc: Aggregate availability par Amadeus query InvSvc->>Amadeus: AirAvailability Amadeus-->>InvSvc: 12 results and Sabre query InvSvc->>Sabre: LowFareSearch Sabre-->>Invsvc: 8 results end InvSvc->>InvSvc: Deduplicate, merge, normalise InvSvc-->>SearchSvc: 18 unique options SearchSvc->>Cache: Cache for 60s end SearchSvc-->>User: 18 flights with prices

Inventory Adapter Pattern

public interface IGdsAdapter
{
    string ProviderName { get; }
    Task<List<FlightAvailability>> SearchFlightsAsync(FlightSearchQuery query);
    Task<List<HotelAvailability>> SearchHotelsAsync(HotelSearchQuery query);
    Task<List<CarAvailability>> SearchCarsAsync(CarSearchQuery query);
    Task<InventoryHoldResult> HoldInventoryAsync(InventoryHoldRequest request);
    Task<bool> ReleaseHoldAsync(string holdLocator);
    Task<BookingConfirmation> ConfirmBookingAsync(BookingConfirmRequest request);
}

public class AmadeusAdapter : IGdsAdapter
{
    private readonly HttpClient _httpClient;
    private readonly AmadeusConfig _config;

    public string ProviderName => "Amadeus";

    public async Task<List<FlightAvailability>> SearchFlightsAsync(FlightSearchQuery query)
    {
        var request = new AmadeusAvailabilityRequest
        {
            Origin = query.OriginCode,
            Destination = query.DestinationCode,
            DepartureDate = query.DepartureDate.ToString("yyyy-MM-dd"),
            Adults = query.AdultCount,
            Children = query.ChildCount,
            TravelClass = MapCabinClass(query.CabinClass),
            NonStopOnly = query.MaxStops == 0
        };

        var response = await _httpClient.PostAsync(
            "/v1/shopping/flight-offers",
            SerializeToJson(request));

        response.EnsureSuccessStatusCode();
        var amadeusResult = await DeserializeAsync<AmadeusShoppingResponse>(response);

        return amadeusResult.Data.Select(MapToAvailability).ToList();
    }

    private FlightAvailability MapToAvailability(AmadeusOffer offer)
    {
        return new FlightAvailability
        {
            Provider = ProviderName,
            ExternalId = offer.Id,
            AirlineCode = offer.ValidatingAirlineCode,
            OriginCode = offer.Itineraries.First().Segments.First().Departure.IataCode,
            DestinationCode = offer.Itineraries.First().Segments.Last().Arrival.IataCode,
            DepartureTime = offer.Itineraries.First().Segments.First().Departure.At,
            ArrivalTime = offer.Itineraries.First().Segments.Last().Arrival.At,
            StopCount = offer.Itineraries.First().Segments.Count - 1,
            BasePrice = decimal.Parse(offer.Price.Base),
            Currency = offer.Price.Currency,
            SeatsAvailable = offer.SeatsAvailable ?? 9,
            FareClass = offer.PricingOptions?.FareType?.FirstOrDefault() ?? "PUBLISHED"
        };
    }
}

Hotel Inventory from OTAs and Direct Connects

Hotel inventory is aggregated from multiple sources including the Hotelbeds API, Booking.com affiliate API, Expedia Rapid API, and direct chain APIs (Marriott, Hilton). Each supplier provides room availability, rate plans, cancellation policies, and static content. The Inventory Service maintains a unified hotel catalogue by matching properties across suppliers using a combination of hotel name fuzzy matching, geographic coordinates, and star rating.

public class HotelInventoryAggregator
{
    private readonly IEnumerable<IHotelSupplier> _suppliers;
    private readonly IHotelMatcher _matcher;

    public async Task<List<UnifiedHotelOffer>> AggregateAsync(HotelSearchQuery query)
    {
        var tasks = _suppliers.Select(s => s.SearchAsync(query));
        var results = await Task.WhenAll(tasks);

        var allOffers = results.SelectMany(r => r).ToList();
        var grouped = allOffers.GroupBy(o => _matcher.GetHotelKey(o));

        return grouped.Select(g => new UnifiedHotelOffer
        {
            HotelId = g.Key,
            HotelName = g.First().HotelName,
            StarRating = g.First().StarRating,
            Location = g.First().Location,
            BestPrice = g.Min(o => o.PricePerNight),
            BestCurrency = g.First(o => o.PricePerNight == g.Min(x => x.PricePerNight)).Currency,
            AvailableSuppliers = g.Select(o => o.Supplier).Distinct().ToList(),
            RoomOptions = g.SelectMany(o => o.RoomOptions).OrderBy(r => r.Price).ToList(),
            FreeCancellation = g.Any(o => o.FreeCancellation),
            BreakfastIncluded = g.Any(o => o.BreakfastIncluded),
            ReviewScore = g.Average(o => o.ReviewScore)
        }).OrderBy(h => h.BestPrice).ToList();
    }
}

Car Rental Inventory

Car rental inventory is sourced from suppliers like Hertz, Avis, Enterprise, and aggregators like CarTrawler. The data model is simpler than flights or hotels — search by pickup/dropoff location and dates, return a list of vehicle categories with daily rates, insurance options, and mileage policies. The Inventory Service must handle one-way rentals where pickup and dropoff locations differ, and must account for local taxes and surcharges that vary by jurisdiction.

Inventory Hold Mechanism

When a user selects an itinerary and proceeds to checkout, the system must hold the inventory atomically. This means reserving the specific seat or room so no other user can book it while the first user enters payment details. The hold has a strict TTL — typically 10 minutes for flights and 15 minutes for hotels — after which the inventory is automatically released. This is implemented using Redis SETNX with TTL and backed by a PostgreSQL row-level lock with a scheduled cleanup job.

public class InventoryHoldService
{
    private readonly IDistributedLockManager _lockManager;
    private readonly TimeSpan _flightHoldTtl = TimeSpan.FromMinutes(10);
    private readonly TimeSpan _hotelHoldTtl = TimeSpan.FromMinutes(15);

    public async Task<HoldResult> HoldInventoryAsync(HoldRequest request)
    {
        var lockKey = $"hold:{request.InventoryType}:{request.InventoryId}";
        
        var acquired = await _lockManager.TryAcquireAsync(
            lockKey,
            request.SessionId,
            request.InventoryType == "FLIGHT" ? _flightHoldTtl : _hotelHoldTtl);

        if (!acquired)
        {
            return new HoldResult
            {
                Success = false,
                Error = "Inventory no longer available. Please search again."
            };
        }

        // Persist hold in database for durability (survives Redis restart)
        await SaveHoldRecordAsync(new InventoryHold
        {
            LockKey = lockKey,
            InventoryType = request.InventoryType,
            InventoryId = request.InventoryId,
            SessionId = request.SessionId,
            UserId = request.UserId,
            ExpiresAt = DateTimeOffset.UtcNow + (request.InventoryType == "FLIGHT" ? _flightHoldTtl : _hotelHoldTtl)
        });

        return new HoldResult
        {
            Success = true,
            HoldId = Guid.NewGuid(),
            ExpiresAt = DateTimeOffset.UtcNow + _flightHoldTtl
        };
    }
}

9. Dynamic Pricing Engine

Travel pricing is inherently dynamic. Airlines change fares hundreds of times per day based on demand curves, remaining seat count, competitor pricing, day-of-week patterns, and historical booking data. Hotels adjust rates based on occupancy, local events, seasonality, and booking window. The pricing engine must calculate the real-time fare at the moment of search and maintain a price lock through checkout to protect both the customer and the business.

Pricing Pipeline

flowchart LR A["Base Fare from GDS"] --> B["Fare Rules & Restrictions"] B --> C["Taxes & Fees by Route"] C --> D["Demand Multiplier Surge"] D --> E["Loyalty & Promotions"] E --> F["Dynamic Markup"] F --> G["Final Price"]

C# Pricing Service Implementation

public class DynamicPricingEngine
{
    private readonly IDemandAnalyzer _demandAnalyzer;
    private readonly IPromotionEngine _promotionEngine;
    private readonly ICurrencyService _currencyService;
    private readonly IPricingConfig _config;

    public async Task<PriceQuote> CalculatePriceAsync(PricingRequest request)
    {
        var baseFare = request.BaseFare;
        baseFare = ApplyFareRules(baseFare, request.FareRules);
        var taxes = await CalculateTaxesAsync(
            request.OriginCode, request.DestinationCode, request.CabinClass);
        var demandMultiplier = await _demandAnalyzer.GetDemandMultiplierAsync(
            request.OriginCode, request.DestinationCode,
            request.DepartureDate, request.CabinClass);
        var surgeAdjustedFare = baseFare * demandMultiplier;
        var loyaltyDiscount = CalculateLoyaltyDiscount(request.UserTier, surgeAdjustedFare);
        var promoDiscount = await _promotionEngine.CalculateDiscountAsync(
            request.UserId, surgeAdjustedFare, request);
        var margin = surgeAdjustedFare * _config.GetMarginPercent(request.OriginCountry);
        var subtotal = surgeAdjustedFare - loyaltyDiscount - promoDiscount;
        var totalPrice = subtotal + taxes + margin;

        return new PriceQuote
        {
            BaseFare = baseFare,
            Taxes = taxes,
            SurgeMultiplier = demandMultiplier,
            SurgeAdjustedFare = surgeAdjustedFare,
            LoyaltyDiscount = loyaltyDiscount,
            PromoDiscount = promoDiscount,
            PlatformMargin = margin,
            TotalPrice = Math.Round(totalPrice, 2),
            Currency = request.Currency,
            ValidUntil = DateTimeOffset.UtcNow.AddMinutes(10),
            PriceLockId = await LockPriceAsync(totalPrice, request)
        };
    }

    private decimal ApplyFareRules(decimal baseFare, List<FareRule> rules)
    {
        foreach (var rule in rules)
        {
            switch (rule.Type)
            {
                case "NON_REFUNDABLE":
                    baseFare *= 0.85m;
                    break;
                case "ADVANCE_PURCHASE":
                    if (rule.DaysInAdvance >= 21)
                        baseFare *= 0.90m;
                    else if (rule.DaysInAdvance >= 7)
                        baseFare *= 0.95m;
                    break;
                case "SATURDAY_NIGHT_STAY":
                    if (rule.HasSaturdayNight)
                        baseFare *= 0.92m;
                    break;
            }
        }
        return baseFare;
    }

    private decimal CalculateLoyaltyDiscount(string tier, decimal fare)
    {
        return tier switch
        {
            "PLATINUM" => fare * 0.10m,
            "GOLD" => fare * 0.07m,
            "SILVER" => fare * 0.05m,
            "BRONZE" => fare * 0.02m,
            _ => 0
        };
    }
}

Demand Multiplier Calculation

The demand multiplier is a real-time signal derived from multiple data sources: historical booking velocity for the same route and date, current search-to-book conversion ratio, remaining inventory percentage, competitor fare levels scraped periodically, and upcoming events at the destination (conferences, holidays, festivals). During peak demand — Christmas flights, summer holidays in Europe, Diwali travel in India — the multiplier can reach 2.0x to 3.5x. During off-peak periods, it drops to 0.85x to attract price-sensitive travellers.

FactorWeightData SourceRefresh Interval
Booking velocity30%Kafka event streamReal-time
Remaining inventory %25%GDS availability5 minutes
Search-to-book ratio20%ClickHouse analytics15 minutes
Competitor pricing15%Price scraper service30 minutes
Seasonal baseline10%Historical data (ClickHouse)Daily recalculation
Fairness Concern: Dynamic pricing must be transparent. Display the price breakdown clearly: base fare, taxes, surge, discounts, and total. Customers should never feel bait and switched between search and checkout. The price lock mechanism guarantees that the displayed price is honoured for 10 minutes. Regulatory bodies in some jurisdictions (e.g., EU Consumer Rights Directive) require that the final price displayed before checkout is the price charged.

10. Booking Flow & State Machine

The booking flow is a multi-step process with strong consistency requirements. Each booking transitions through a well-defined state machine, and the entire flow is implemented as a distributed saga to handle partial failures gracefully across the Inventory, Pricing, Booking, and Payment services.

Booking State Machine

stateDiagram-v2 [*] --> DRAFT: User starts booking DRAFT --> PRICE_LOCKED: Price lock acquired PRICE_LOCKED --> PAYMENT_PENDING: User confirms enters payment PAYMENT_PENDING --> PAYMENT_PROCESSED: Payment succeeds PAYMENT_PENDING --> PAYMENT_FAILED: Payment fails PAYMENT_FAILED --> DRAFT: Retry payment PAYMENT_PROCESSED --> INVENTORY_CONFIRMED: GDS confirms booking PAYMENT_PROCESSED --> PAYMENT_REFUNDED: GDS rejects booking INVENTORY_CONFIRMED --> CONFIRMED: All legs confirmed CONFIRMED --> CANCELLATION_PENDING: User requests cancel CANCELLATION_PENDING --> CANCELLED: Cancellation processed CANCELLATION_PENDING --> REFUND_PENDING: Refund required REFUND_PENDING --> REFUNDED: Refund completed CONFIRMED --> REBOOKING_PENDING: User requests rebook REBOOKING_PENDING --> REBOOKED: Rebooking confirmed PAYMENT_REFUNDED --> [*] CANCELLED --> [*] REBOOKED --> CONFIRMED

Saga Orchestrator Implementation

public class BookingSagaOrchestrator
{
    private readonly IInventoryService _inventoryService;
    private readonly IPricingService _pricingService;
    private readonly IPaymentService _paymentService;
    private readonly IBookingRepository _bookingRepo;
    private readonly IEventBus _eventBus;

    public async Task<BookingResult> ExecuteBookingAsync(BookingCommand command)
    {
        var sagaId = Guid.NewGuid();
        var booking = new Booking
        {
            BookingId = Guid.NewGuid(),
            UserId = command.UserId,
            Status = BookingStatus.DRAFT,
            SagaId = sagaId
        };

        try
        {
            // Step 1: Validate and lock price
            var priceLock = await _pricingService.LockPriceAsync(new PriceLockRequest
            {
                SessionId = command.SessionId,
                InventoryItems = command.Legs.Select(l => l.InventoryId).ToList(),
                Amount = command.ExpectedTotal,
                Currency = command.Currency
            });

            booking.Status = BookingStatus.PRICE_LOCKED;
            booking.PriceLockId = priceLock.LockId;
            await _bookingRepo.SaveAsync(booking);

            // Step 2: Process payment
            var payment = await _paymentService.ChargeAsync(new PaymentRequest
            {
                UserId = command.UserId,
                Amount = command.ExpectedTotal,
                Currency = command.Currency,
                PaymentMethodId = command.PaymentMethodId,
                IdempotencyKey = $"booking-{booking.BookingId}"
            });

            if (payment.Status != PaymentStatus.SUCCESS)
            {
                booking.Status = BookingStatus.PAYMENT_FAILED;
                await _bookingRepo.SaveAsync(booking);
                await _pricingService.ReleaseLockAsync(priceLock.LockId);
                return new BookingResult { Success = false, Error = "Payment failed" };
            }

            booking.Status = BookingStatus.PAYMENT_PROCESSED;
            booking.PaymentId = payment.PaymentId;
            await _bookingRepo.SaveAsync(booking);

            // Step 3: Confirm inventory with GDS
            var confirmations = new List<LegConfirmation>();
            foreach (var leg in command.Legs)
            {
                var confirmation = await _inventoryService.ConfirmAsync(new ConfirmRequest
                {
                    InventoryId = leg.InventoryId,
                    Passengers = leg.Passengers,
                    ContactInfo = command.ContactInfo,
                    PriceLockId = priceLock.LockId
                });
                confirmations.Add(confirmation);
            }

            booking.Status = BookingStatus.INVENTORY_CONFIRMED;
            booking.Legs = confirmations.Select(c => new BookingLeg
            {
                LegId = Guid.NewGuid(),
                SupplierConfirmation = c.ConfirmationNumber,
                Status = LegStatus.CONFIRMED
            }).ToList();
            await _bookingRepo.SaveAsync(booking);

            // Step 4: Final confirmation
            booking.Status = BookingStatus.CONFIRMED;
            booking.BookingRef = GenerateBookingRef();
            booking.BookedAt = DateTimeOffset.UtcNow;
            await _bookingRepo.SaveAsync(booking);

            // Step 5: Emit events
            await _eventBus.PublishAsync(new BookingConfirmedEvent
            {
                BookingId = booking.BookingId,
                UserId = booking.UserId,
                BookingRef = booking.BookingRef
            });

            return new BookingResult { Success = true, Booking = booking };
        }
        catch (Exception ex)
        {
            await CompensateAsync(booking, command);
            throw;
        }
    }

    private async Task CompensateAsync(Booking booking, BookingCommand command)
    {
        if (booking.Status == BookingStatus.PAYMENT_PROCESSED ||
            booking.Status == BookingStatus.INVENTORY_CONFIRMED)
        {
            if (booking.PaymentId.HasValue)
            {
                await _paymentService.RefundAsync(new RefundRequest
                {
                    PaymentId = booking.PaymentId.Value,
                    Amount = command.ExpectedTotal,
                    Reason = "Booking saga compensation"
                });
            }
        }

        if (booking.PriceLockId.HasValue)
        {
            await _pricingService.ReleaseLockAsync(booking.PriceLockId.Value);
        }

        booking.Status = BookingStatus.FAILED;
        await _bookingRepo.SaveAsync(booking);
    }

    private string GenerateBookingRef()
    {
        return $"TRV{DateTimeOffset.UtcNow:yyyyMMdd}{Guid.NewGuid().ToString("N")[..6].ToUpper()}";
    }
}

Booking Flow Sequence

sequenceDiagram participant User participant API as API Gateway participant BookSvc as Booking Service participant PriceSvc as Pricing Service participant PaySvc as Payment Service participant InvSvc as Inventory Service participant Kafka User->>API: Confirm Booking API->>BookSvc: CreateBookingCommand BookSvc->>PriceSvc: LockPrice (10 min TTL) PriceSvc-->>BookSvc: PriceLock confirmed BookSvc->>PaySvc: ChargePayment PaySvc-->>BookSvc: PaymentSuccess BookSvc->>InvSvc: ConfirmInventory InvSvc-->>BookSvc: GDSConfirmation BookSvc->>BookSvc: Update status to CONFIRMED BookSvc->>Kafka: BookingConfirmedEvent Kafka-->>BookSvc: Event published BookSvc-->>API: BookingResult API-->>User: Confirmation page and email

11. Payment & Refunds

Payment processing in travel is uniquely challenging because of the high transaction values, multi-currency requirements, PCI-DSS compliance mandates, and the need for split payments (airline fare vs. taxes vs. service fee). The Payment Service must integrate with multiple gateways — Stripe for cards, Razorpay for UPI in India, PayPal for digital wallets, Adyen for European payment methods — and handle 3D Secure authentication, tokenised card storage, and multi-capture flows where inventory confirmation and payment capture happen asynchronously.

Payment Flow

flowchart TD A["User submits payment"] --> B["Fraud check"] B -->|Pass| C["Tokenise card via Stripe.js"] B -->|Fail| Z["Reject payment"] C --> D["3D Secure authentication"] D -->|Authenticated| E["Capture payment via gateway"] D -->|Failed| F["Request OTP fallback"] F --> E E -->|Success| G["Payment confirmed"] E -->|Insufficient funds| H["Notify user try another method"] E -->|Gateway error| I["Retry with secondary gateway"] G --> J["Release to booking service"] J --> K["GDS ticket issuance"]

Payment Service Implementation

public class PaymentService : IPaymentService
{
    private readonly IEnumerable<IPaymentGateway> _gateways;
    private readonly IFraudDetector _fraudDetector;
    private readonly IPaymentRepository _repo;

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        var fraudScore = await _fraudDetector.EvaluateAsync(request);
        if (fraudScore.IsHighRisk)
        {
            await LogFraudEventAsync(request, fraudScore);
            return new PaymentResult
            {
                Status = PaymentStatus.REJECTED,
                Reason = "Payment flagged for review"
            };
        }

        var gateway = SelectGateway(request.PaymentMethod, request.Country);

        PaymentResult result = null;
        var attempts = 0;
        var maxAttempts = 2;

        while (attempts < maxAttempts)
        {
            try
            {
                result = await gateway.ChargeAsync(new GatewayChargeRequest
                {
                    Amount = request.Amount,
                    Currency = request.Currency,
                    CustomerId = request.UserId.ToString(),
                    PaymentToken = request.PaymentToken,
                    IdempotencyKey = request.IdempotencyKey,
                    Metadata = new Dictionary<string, string>
                    {
                        ["booking_id"] = request.BookingId.ToString(),
                        ["user_email"] = request.UserEmail
                    }
                });
                break;
            }
            catch (GatewayException ex) when (ex.IsTransient)
            {
                attempts++;
                if (attempts >= maxAttempts)
                    gateway = GetFailoverGateway(request.PaymentMethod);
            }
        }

        await _repo.SaveTransactionAsync(new PaymentTransaction
        {
            PaymentId = Guid.NewGuid(),
            BookingId = request.BookingId,
            Amount = request.Amount,
            Currency = request.Currency,
            Gateway = gateway.Name,
            GatewayTransactionId = result.GatewayTxnId,
            Status = result.Status,
            CardLastFour = request.CardLastFour,
            CardBrand = request.CardBrand,
            ProcessedAt = DateTimeOffset.UtcNow
        });

        return result;
    }

    public async Task<RefundResult> ProcessRefundAsync(RefundRequest request)
    {
        var original = await _repo.GetTransactionAsync(request.PaymentId);
        var gateway = GetGatewayByName(original.Gateway);

        var refundAmount = request.Amount ?? original.Amount;
        var cancellationFee = CalculateCancellationFee(original.BookingId, refundAmount);

        var result = await gateway.RefundAsync(new GatewayRefundRequest
        {
            OriginalTransactionId = original.GatewayTransactionId,
            Amount = refundAmount - cancellationFee,
            Reason = request.Reason,
            IdempotencyKey = $"refund-{request.PaymentId}-{DateTimeOffset.UtcNow.Ticks}"
        });

        await _repo.SaveRefundAsync(new RefundTransaction
        {
            RefundId = Guid.NewGuid(),
            OriginalPaymentId = request.PaymentId,
            RefundAmount = refundAmount - cancellationFee,
            CancellationFee = cancellationFee,
            Status = result.Status,
            GatewayRefundId = result.GatewayRefundId,
            ProcessedAt = DateTimeOffset.UtcNow
        });

        return new RefundResult
        {
            Success = result.Status == RefundStatus.SUCCESS,
            RefundAmount = refundAmount - cancellationFee,
            CancellationFee = cancellationFee,
            EstimatedRefundDate = DateTimeOffset.UtcNow.AddDays(5)
        };
    }

    private decimal CalculateCancellationFee(Guid bookingId, decimal totalAmount)
    {
        var hoursUntilDeparture = GetHoursUntilDeparture(bookingId);
        return hoursUntilDeparture switch
        {
            > 72 => totalAmount * 0.10m,
            > 24 => totalAmount * 0.25m,
            > 6  => totalAmount * 0.50m,
            _    => totalAmount * 0.80m
        };
    }
}

PCI-DSS Compliance Measures

RequirementImplementationStatus
Card data never touches our serversStripe.js / Adyen Drop-in for tokenisationCompliant
Encrypted storage of tokensAWS KMS envelope encryptionCompliant
Network segmentationVPC isolation, WAF rules on payment endpointsCompliant
Access loggingAll payment API calls logged to immutable audit trailCompliant
Annual PCI auditSAQ-A for card-not-present environmentCompliant

12. Itinerary Management

Once a booking is confirmed, the Itinerary Service generates a comprehensive travel itinerary that aggregates all booking legs, travel documents, local information, and real-time status updates. The itinerary serves as the single source of truth for the traveller and is rendered as a PDF, displayed in the mobile app, and sent via email. It must be available offline for travellers who lose connectivity during their trip.

Itinerary Data Model

public class TravelItinerary
{
    public Guid ItineraryId { get; set; }
    public Guid BookingId { get; set; }
    public string BookingRef { get; set; }
    public Guid UserId { get; set; }
    public List<ItineraryDay> Days { get; set; }
    public List<TravellerInfo> Travellers { get; set; }
    public EmergencyContact EmergencyContact { get; set; }
    public List<TravelDocument> Documents { get; set; }
    public List<ItineraryAlert> ActiveAlerts { get; set; }
    public ItineraryStatus Status { get; set; }
    public DateTimeOffset CreatedAt { get; set; }
    public DateTimeOffset? TripStartDate { get; set; }
    public DateTimeOffset? TripEndDate { get; set; }
}

public class ItineraryDay
{
    public DateTime Date { get; set; }
    public List<ItineraryEvent> Events { get; set; }
}

public class ItineraryEvent
{
    public Guid EventId { get; set; }
    public EventType Type { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public DateTimeOffset StartTime { get; set; }
    public DateTimeOffset? EndTime { get; set; }
    public Location Location { get; set; }
    public string ConfirmationNumber { get; set; }
    public string MapUrl { get; set; }
    public List<ItineraryDocument> Attachments { get; set; }
    public EventStatus Status { get; set; }
}

public class ItineraryAlert
{
    public Guid AlertId { get; set; }
    public AlertSeverity Severity { get; set; }
    public string Title { get; set; }
    public string Message { get; set; }
    public DateTimeOffset OccurredAt { get; set; }
    public DateTimeOffset? ResolvedAt { get; set; }
    public List<ActionOption> SuggestedActions { get; set; }
}

public class TravelDocument
{
    public DocumentType Type { get; set; }
    public string FileName { get; set; }
    public string StorageUrl { get; set; }
    public DateTimeOffset GeneratedAt { get; set; }
}

Itinerary Features

  • Day-by-day view: Groups all events (flights, hotel check-ins, car pickups) by calendar day with local time zones. Users can toggle between their home timezone and local timezone for the destination.
  • Offline access: The mobile app caches the itinerary for offline access during travel when connectivity may be limited. A background sync job updates cached data when connectivity resumes.
  • Real-time updates: Push notifications for gate changes, delays, and check-in reminders via WebSocket connections. The notification service listens to Kafka events from the Inventory Service and pushes updates to connected clients.
  • Document attachment: E-tickets, hotel vouchers, car rental agreements, visa copies, and insurance documents stored as PDFs in S3 with pre-signed URLs for secure access.
  • Share with companions: Generate a shareable link so travel companions can view the same itinerary without an account. Share links are time-limited and can be revoked.
  • Calendar integration: One-tap export to Google Calendar, Apple Calendar, or Outlook via .ics file generation.

13. Review & Rating System

The review and rating system builds trust and drives conversion. It must support verified bookings (only users who actually stayed at a hotel or flew on a flight can leave reviews), prevent duplicate reviews, aggregate ratings across multiple dimensions, and surface the most helpful reviews via a ranking algorithm. The system also handles review moderation to filter out offensive content and detect fake reviews using NLP analysis.

Multi-Dimensional Rating

EntityRating DimensionsScale
HotelCleanliness, Comfort, Location, Staff, Value, Amenities1-5 stars
FlightSeat comfort, Service, Punctuality, Food, Entertainment1-5 stars
Car RentalVehicle condition, Pick-up speed, Customer service, Value1-5 stars
public class ReviewService : IReviewService
{
    private readonly IReviewRepository _repo;
    private readonly IBookingRepository _bookingRepo;
    private readonly IReviewRanker _ranker;

    public async Task<ReviewResult> SubmitReviewAsync(ReviewSubmission submission)
    {
        var booking = await _bookingRepo.FindBookingAsync(
            submission.UserId, submission.EntityType, submission.EntityId);

        if (booking == null || booking.Status != BookingStatus.CONFIRMED)
        {
            return new ReviewResult
            {
                Success = false,
                Error = "Only verified bookings can be reviewed"
            };
        }

        var existing = await _repo.FindByUserAndEntityAsync(
            submission.UserId, submission.EntityType, submission.EntityId);

        if (existing != null)
        {
            return new ReviewResult
            {
                Success = false,
                Error = "You have already reviewed this booking"
            };
        }

        if (!IsReviewEligible(booking))
        {
            return new ReviewResult
            {
                Success = false,
                Error = "Reviews can be submitted 24 hours after completion"
            };
        }

        var review = new Review
        {
            ReviewId = Guid.NewGuid(),
            UserId = submission.UserId,
            EntityType = submission.EntityType,
            EntityId = submission.EntityId,
            Rating = submission.Rating,
            DimensionRatings = submission.DimensionRatings,
            Title = submission.Title,
            Body = submission.Body,
            IsVerifiedBooking = true,
            CreatedAt = DateTimeOffset.UtcNow
        };

        await _repo.SaveAsync(review);
        await RecalculateAggregateRatingAsync(submission.EntityType, submission.EntityId);

        return new ReviewResult { Success = true, ReviewId = review.ReviewId };
    }

    private bool IsReviewEligible(Booking booking)
    {
        var lastLegEnd = booking.Legs.Max(l => l.ArrivalTime);
        return DateTimeOffset.UtcNow >= lastLegEnd.AddHours(24);
    }
}

Review Ranking Algorithm

Reviews are ranked using a composite score that considers recency (more recent reviews rank higher), helpfulness (upvotes from other users), verified booking status (verified reviews are boosted), rating extremity (very positive and very negative reviews tend to be more informative), and reviewer credibility (users with a history of helpful reviews get a boost). The ranking algorithm is periodically retrained using A/B tests to optimise for the metric that correlates most strongly with booking conversion.

14. Travel Alerts & Notifications

Real-time travel alerts keep passengers informed about disruptions, gate changes, weather events, and visa requirement updates. The Notification Service consumes events from Kafka topics and fans out to multiple delivery channels: email (via Amazon SES), SMS (via Twilio), push notifications (via Firebase Cloud Messaging), and in-app real-time messages (via WebSockets).

Alert Types and Routing

Alert TypeChannelsPrioritySource
Flight delayPush, SMS, EmailHighAirline GDS feed
Gate changePush, SMSHighAirport systems
Flight cancellationPush, SMS, EmailCriticalAirline GDS feed
Booking confirmationEmailNormalBooking Service
Check-in reminderPush, EmailNormalScheduled job
Hotel check-in timePushLowScheduled job
Weather warningPush, EmailMediumWeather API
Visa requirement changeEmailMediumImmigration API
Price drop alertPush, EmailLowPricing Service
Review reminderPush, EmailLowScheduled job
public class NotificationService
{
    private readonly IEmailProvider _email;
    private readonly ISmsProvider _sms;
    private readonly IPushProvider _push;
    private readonly IWebSocketHub _wsHub;
    private readonly INotificationPreferenceRepository _prefs;

    public async Task SendAlertAsync(TravelAlert alert)
    {
        var preferences = await _prefs.GetForUserAsync(alert.UserId);
        var tasks = new List<Task>();

        if (preferences.PushEnabled && alert.Priority >= Priority.Normal)
        {
            tasks.Add(_push.SendAsync(new PushMessage
            {
                UserId = alert.UserId,
                Title = alert.Title,
                Body = alert.Message,
                Data = alert.Metadata,
                ChannelId = "travel-alerts",
                Priority = alert.Priority == Priority.Critical ? "high" : "normal"
            }));
        }

        if (preferences.SmsEnabled && alert.Priority >= Priority.High)
        {
            tasks.Add(_sms.SendAsync(new SmsMessage
            {
                PhoneNumber = preferences.PhoneNumber,
                Body = $"{alert.Title}: {alert.Message}",
                SenderId = "TRVLBOOK"
            }));
        }

        if (alert.RequiresEmail || preferences.EmailEnabled)
        {
            tasks.Add(_email.SendAsync(new EmailMessage
            {
                To = preferences.EmailAddress,
                Subject = alert.Title,
                TemplateId = alert.EmailTemplateId,
                TemplateData = alert.TemplateData
            }));
        }

        if (preferences.InAppEnabled)
        {
            await _wsHub.SendToUserAsync(alert.UserId, new WsMessage
            {
                Type = "TRAVEL_ALERT",
                Payload = alert
            });
        }

        await Task.WhenAll(tasks);
    }
}

Alert Delivery SLAs

PriorityTarget Delivery TimeChannel Fallback
Critical (cancellation)< 30 secondsPush fails -> SMS -> Email
High (delay, gate change)< 1 minutePush fails -> SMS
Medium (weather, visa)< 5 minutesPush fails -> Email
Low (reminders, promos)< 30 minutesEmail only

15. Multi-Currency & Localization

A global travel platform must support dozens of currencies and localise content for different markets. Currency conversion must use real-time exchange rates with a small markup to cover forex risk. Prices displayed during search must be in the user's preferred currency, and the final payment must be in the currency supported by the user's payment method. Localization extends beyond currency — date formats, number formatting, language, and regulatory text (terms and conditions, visa warnings) must all be adapted to the user's locale.

Currency Architecture

public class CurrencyService : ICurrencyService
{
    private readonly IExchangeRateProvider _rateProvider;
    private readonly IDistributedCache _cache;
    private const decimal MarkupPercent = 0.015m;

    public async Task<decimal> ConvertAsync(
        decimal amount, string fromCurrency, string toCurrency)
    {
        if (fromCurrency == toCurrency) return amount;
        var rate = await GetExchangeRateAsync(fromCurrency, toCurrency);
        var converted = amount * rate;
        var withMarkup = converted * (1 + MarkupPercent);
        return Math.Round(withMarkup, GetDecimalPlaces(toCurrency));
    }

    public async Task<Dictionary<string, decimal>> GetMultiCurrencyPricesAsync(
        decimal baseAmount, string baseCurrency, List<string> targetCurrencies)
    {
        var tasks = targetCurrencies
            .Where(c => c != baseCurrency)
            .Select(async c => new { Currency = c, Amount = await ConvertAsync(baseAmount, baseCurrency, c) });
        var results = await Task.WhenAll(tasks);
        return results.ToDictionary(r => r.Currency, r => r.Amount);
    }

    private async Task<decimal> GetExchangeRateAsync(string from, string to)
    {
        var cacheKey = $"fx:{from}:{to}";
        var cached = await _cache.GetAsync<decimal?>(cacheKey);
        if (cached.HasValue) return cached.Value;
        var rate = await _rateProvider.GetRateAsync(from, to);
        await _cache.SetAsync(cacheKey, rate, TimeSpan.FromMinutes(15));
        return rate;
    }

    private int GetDecimalPlaces(string currency) => currency switch
    {
        "JPY" or "KRW" or "VND" => 0,
        "BHD" or "KWD" or "OMR" => 3,
        _ => 2
    };
}

Supported Currencies and Regions

RegionPrimary CurrenciesPayment MethodsRegulatory Notes
North AmericaUSD, CADCards, PayPal, Apple PayPCI-DSS, SOC 2
EuropeEUR, GBP, CHFCards, iDEAL, BancontactPSD2 SCA required
IndiaINRUPI, Cards, Net Banking, EMIRBI guidelines, GST
Southeast AsiaSGD, MYR, THB, IDRCards, GrabPay, GCashData residency
Middle EastAED, SAR, QARCards, Mada, STC PayVAT compliance
JapanJPYCards, Konbini, PayPayJapanese language required
LATAMBRL, MXN, ARSCards, Boleto, OXXOInstallment plans popular
Localization Tip: Always store prices in the base currency (usually USD) and convert on-the-fly for display. Never store converted prices as the source of truth, as exchange rates change frequently. When the user pays, convert at the current rate with a small forex buffer to protect against rate fluctuations between display and capture.

16. Loyalty Program

A loyalty program drives repeat bookings and increases customer lifetime value. Points are earned per dollar spent and can be redeemed for discounts, upgrades, or free bookings. The program has tier levels (Bronze, Silver, Gold, Platinum) that unlock benefits like priority support, lounge access, and bonus points multipliers. Points expire after 24 months of account inactivity to encourage regular engagement.

Loyalty Points Engine

public class LoyaltyService
{
    private readonly ILoyaltyRepository _repo;
    private readonly IPricingService _pricing;

    public async Task<PointsResult> EarnPointsAsync(EarnPointsCommand command)
    {
        var account = await _repo.GetAccountAsync(command.UserId);
        var basePoints = CalculateBasePoints(command.Amount, command.BookingType);
        var multiplier = GetTierMultiplier(account.Tier);
        var bonusPoints = await CalculateBonusPointsAsync(command);
        var totalEarned = (long)((basePoints * multiplier) + bonusPoints);

        account.PointsBalance += totalEarned;
        account.LifetimePoints += totalEarned;
        account.Tier = CalculateNewTier(account.LifetimePoints);
        await _repo.UpdateAsync(account);

        await _repo.AddTransactionAsync(new LoyaltyTransaction
        {
            AccountId = account.AccountId,
            Type = LoyaltyTransactionType.EARN,
            Points = totalEarned,
            BookingId = command.BookingId,
            Description = $"Points earned on {command.BookingType} booking"
        });

        return new PointsResult
        {
            BasePoints = basePoints,
            TierMultiplier = multiplier,
            BonusPoints = bonusPoints,
            TotalEarned = totalEarned,
            NewBalance = account.PointsBalance,
            NewTier = account.Tier
        };
    }

    public async Task<RedemptionResult> RedeemPointsAsync(RedeemPointsCommand command)
    {
        var account = await _repo.GetAccountAsync(command.UserId);
        if (account.PointsBalance < command.PointsToRedeem)
            return new RedemptionResult { Success = false, Error = "Insufficient points" };

        var redemptionValue = command.PointsToRedeem * 0.01m;
        var bookingAmount = await _pricing.GetBookingAmountAsync(command.BookingId);

        if (redemptionValue > bookingAmount * 0.50m)
        {
            redemptionValue = bookingAmount * 0.50m;
            command.PointsToRedeem = (long)(redemptionValue / 0.01m);
        }

        account.PointsBalance -= command.PointsToRedeem;
        await _repo.UpdateAsync(account);

        return new RedemptionResult
        {
            Success = true,
            PointsRedeemed = command.PointsToRedeemed,
            MonetaryValue = redemptionValue,
            RemainingBalance = account.PointsBalance
        };
    }

    private long CalculateBasePoints(decimal amount, string bookingType)
    {
        var pointsPerDollar = bookingType switch
        {
            "FLIGHT" => 5,
            "HOTEL" => 8,
            "CAR" => 3,
            "PACKAGE" => 10,
            _ => 5
        };
        return (long)(amount * pointsPerDollar);
    }

    private decimal GetTierMultiplier(string tier) => tier switch
    {
        "PLATINUM" => 2.0m,
        "GOLD" => 1.5m,
        "SILVER" => 1.2m,
        "BRONZE" => 1.0m,
        _ => 1.0m
    };

    private string CalculateNewTier(long lifetimePoints) => lifetimePoints switch
    {
        >= 100_000 => "PLATINUM",
        >= 50_000 => "GOLD",
        >= 10_000 => "SILVER",
        _ => "BRONZE"
    };
}

Loyalty Tier Benefits

TierLifetime PointsPoints MultiplierBenefits
Bronze0 - 9,9991.0xBasic earning, standard support
Silver10,000 - 49,9991.2xPriority check-in, 5% hotel discount
Gold50,000 - 99,9991.5xLounge access, free seat selection, 10% discount
Platinum100,000+2.0xFree upgrade, dedicated support, 15% discount, early access to deals

17. Partner API Integration

The Partner API enables white-label integration for online travel agencies (OTAs), corporate travel managers, and affiliate marketers. Partners authenticate via OAuth 2.0 client credentials, search and book inventory through our platform, and receive webhook notifications for booking status changes. Rate limiting and usage-based billing ensure fair resource allocation across partners.

Partner API Implementation

public class PartnerApiController : ControllerBase
{
    private readonly IPartnerService _partnerService;
    private readonly IRateLimiter _rateLimiter;

    [HttpPost("search")]
    [ServiceFilter(typeof(PartnerAuthFilter))]
    public async Task<ActionResult<PartnerSearchResponse>> PartnerSearch(
        [FromBody] PartnerSearchRequest request)
    {
        var partner = HttpContext.Items["Partner"] as Partner;
        var rateLimitResult = await _rateLimiter.CheckAsync(
            partner.PartnerId, "search", partner.SearchQuota);
        
        if (!rateLimitResult.Allowed)
        {
            return StatusCode(429, new { error = "Rate limit exceeded",
                retry_after = rateLimitResult.RetryAfterSeconds });
        }

        var results = await _partnerService.SearchAsync(request, partner);
        var markedUp = ApplyPartnerMarkup(results, partner.CommissionPercent);

        return Ok(new PartnerSearchResponse
        {
            Results = markedUp,
            Quota = new QuotaInfo
            {
                Remaining = rateLimitResult.Remaining,
                ResetsAt = rateLimitResult.ResetsAt
            }
        });
    }

    [HttpPost("book")]
    [ServiceFilter(typeof(PartnerAuthFilter))]
    public async Task<ActionResult<PartnerBookingResponse>> PartnerBook(
        [FromBody] PartnerBookingRequest request)
    {
        var partner = HttpContext.Items["Partner"] as Partner;
        var result = await _partnerService.BookAsync(request, partner);

        await SendWebhookAsync(partner.WebhookUrl, new PartnerWebhookPayload
        {
            EventType = "BOOKING_CONFIRMED",
            BookingRef = result.BookingRef,
            Timestamp = DateTimeOffset.UtcNow
        });

        return Ok(result);
    }

    private List<SearchResult> ApplyPartnerMarkup(List<SearchResult> results, decimal commission)
    {
        foreach (var result in results)
            result.DisplayPrice = result.BasePrice * (1 + commission / 100);
        return results;
    }
}

Partner Tiers and Rate Limits

Partner TierMonthly FeeSearch QPSBooking QPSCommission
StarterFree1025%
Professional$500/month100203%
EnterpriseCustom1000100Negotiated

Webhook Integration

Partners receive real-time notifications via webhooks for booking confirmations, cancellations, refunds, and travel alerts. Each webhook payload is signed with HMAC-SHA256 using a shared secret, allowing partners to verify authenticity. Failed webhook deliveries are retried with exponential backoff (3 attempts over 1 hour) and logged for debugging. Partners can register up to 3 webhook URLs for redundancy.

18. Cancellation & Rebooking

Cancellation is a complex business process governed by fare rules, timing, and refund policies. Each fare class has specific cancellation terms — some fares are fully refundable, some carry cancellation fees, and some are entirely non-refundable. The cancellation flow must coordinate across the Booking Service, Payment Service, and Inventory Service to release inventory back to the GDS and process refunds.

Cancellation Flow

flowchart TD A["User requests cancel"] --> B{"Check fare rules"} B -->|"Fully refundable"| C["100% refund"] B -->|"Cancellation fee"| D["Calculate fee based on timing"] B -->|"Non-refundable"| E["Notify user no refund"] D --> F["Refund = Total - Fee"] C --> G["Release inventory to GDS"] F --> G E --> H["Cancel booking status only"] G --> I["Update status to CANCELLED"] H --> I I --> J["Send confirmation email"] J --> K["Process refund via Payment Service"]

Rebooking Implementation

public class RebookingService
{
    private readonly IBookingRepository _bookingRepo;
    private readonly IInventoryService _inventoryService;
    private readonly IPricingService _pricingService;
    private readonly IPaymentService _paymentService;

    public async Task<RebookingResult> RebookAsync(RebookRequest request)
    {
        var originalBooking = await _bookingRepo.GetAsync(request.BookingId);
        if (originalBooking.UserId != request.UserId)
            throw new UnauthorizedException("Not your booking");
        if (originalBooking.Status != BookingStatus.CONFIRMED)
            throw new InvalidOperationException("Only confirmed bookings can be rebooked");

        var newAvailability = await _inventoryService.SearchAsync(new SearchRequest
        {
            OriginCode = originalBooking.Legs.First().OriginCode,
            DestinationCode = originalBooking.Legs.First().DestinationCode,
            DepartureDate = request.NewDepartureDate,
            PassengerCount = originalBooking.Legs.Sum(l => l.PassengerCount)
        });

        if (!newAvailability.Any())
            return new RebookingResult { Success = false, Error = "No availability for requested dates" };

        var bestOption = newAvailability.First();
        var originalTotal = originalBooking.Legs.Sum(l => l.TotalFare);
        var fareDifference = bestOption.TotalPrice - originalTotal;
        var rebookingFee = CalculateRebookingFee(originalBooking);

        if (fareDifference > 0)
        {
            var totalDue = fareDifference + rebookingFee;
            var payment = await _paymentService.ChargeAsync(new PaymentRequest
            {
                UserId = request.UserId,
                Amount = totalDue,
                Currency = originalBooking.Currency,
                PaymentMethodId = request.PaymentMethodId,
                IdempotencyKey = $"rebook-{request.BookingId}-{DateTimeOffset.UtcNow.Ticks}"
            });
            if (payment.Status != PaymentStatus.SUCCESS)
                return new RebookingResult { Success = false, Error = "Payment for fare difference failed" };
        }
        else if (fareDifference < 0)
        {
            var refundAmount = Math.Abs(fareDifference) - rebookingFee;
            if (refundAmount > 0)
            {
                await _paymentService.RefundAsync(new RefundRequest
                {
                    PaymentId = originalBooking.PaymentId.Value,
                    Amount = refundAmount,
                    Reason = "Rebooking fare difference refund"
                });
            }
        }

        foreach (var leg in originalBooking.Legs)
            await _inventoryService.CancelAsync(leg.SupplierConfirmation);

        var newConfirmation = await _inventoryService.ConfirmAsync(new ConfirmRequest
        {
            InventoryId = bestOption.InventoryId,
            Passengers = originalBooking.Legs.First().Passengers
        });

        originalBooking.Legs = new List<BookingLeg>
        {
            new BookingLeg
            {
                LegId = Guid.NewGuid(),
                SupplierConfirmation = newConfirmation.ConfirmationNumber,
                DepartureTime = bestOption.DepartureTime,
                ArrivalTime = bestOption.ArrivalTime,
                TotalFare = bestOption.TotalPrice,
                Status = LegStatus.CONFIRMED
            }
        };
        originalBooking.TotalAmount = bestOption.TotalPrice;
        originalBooking.Status = BookingStatus.CONFIRMED;
        await _bookingRepo.UpdateAsync(originalBooking);

        return new RebookingResult
        {
            Success = true,
            FareDifference = fareDifference,
            RebookingFee = rebookingFee,
            NewBookingRef = originalBooking.BookingRef
        };
    }

    private decimal CalculateRebookingFee(Booking booking)
    {
        var hoursUntilDeparture = booking.Legs.Min(l =>
            (l.DepartureTime - DateTimeOffset.UtcNow).TotalHours);
        return hoursUntilDeparture switch
        {
            > 72  => 25.00m,
            > 24  => 50.00m,
            > 6   => 100.00m,
            _     => 0
        };
    }
}

19. Fraud Detection

Travel booking fraud is a significant threat — stolen credit cards, account takeover, and loyalty point theft cost the industry billions annually. The Fraud Detection Service runs in real-time as part of the payment flow, analysing transaction patterns, device fingerprints, IP geolocation, and user behaviour to assign a risk score to every booking attempt. High-risk transactions are flagged for manual review or blocked outright.

Fraud Signals and Scoring

public class FraudDetector : IFraudDetector
{
    private readonly IDeviceFingerprintService _deviceService;
    private readonly IVelocityChecker _velocityChecker;
    private readonly IBinLookupService _binLookup;
    private readonly IMLModelRunner _mlRunner;

    public async Task<FraudAssessment> EvaluateAsync(PaymentRequest request)
    {
        var signals = new FraudSignals();

        var device = await _deviceService.GetFingerprintAsync(request.DeviceFingerprint);
        signals.IsNewDevice = device.FirstSeenDaysAgo < 3;
        signals.IsKnownFraudDevice = device.IsFlagged;
        signals.DeviceReputationScore = device.ReputationScore;

        signals.PaymentsLastHour = await _velocityChecker.CountAsync(
            request.UserId, "payment", TimeSpan.FromHours(1));
        signals.PaymentsLastDay = await _velocityChecker.CountAsync(
            request.UserId, "payment", TimeSpan.FromDays(1));
        signals.SameCardMultipleAccounts = await _velocityChecker.CheckCardSpreadAsync(
            request.CardLastFour, maxAccounts: 3, window: TimeSpan.FromDays(7));

        var bin = await _binLookup.LookupAsync(request.CardBIN);
        signals.CardCountry = bin.IssuingCountry;
        signals.IpCountry = request.IpCountry;
        signals.CountryMismatch = bin.IssuingCountry != request.IpCountry;

        signals.LastMinuteBooking = request.BookingDate - request.DepartureDate <
            TimeSpan.FromHours(24);
        signals.HighValueBooking = request.Amount > 2000;
        signals.OneWayInternational = request.IsOneWay && request.IsInternational;

        var features = BuildFeatureVector(signals);
        var mlScore = await _mlRunner.PredictFraudAsync(features);
        var riskScore = CalculateCompositeRiskScore(signals, mlScore);

        return new FraudAssessment
        {
            RiskScore = riskScore,
            IsHighRisk = riskScore > 0.7,
            RequiresManualReview = riskScore > 0.5 && riskScore <= 0.7,
            Signals = signals,
            Recommendation = riskScore switch
            {
                > 0.8 => FraudDecision.BLOCK,
                > 0.5 => FraudDecision.REVIEW,
                _ => FraudDecision.APPROVE
            }
        };
    }

    private decimal CalculateCompositeRiskScore(FraudSignals signals, float mlScore)
    {
        var score = mlScore * 0.4m;
        if (signals.IsKnownFraudDevice) score += 0.3m;
        if (signals.PaymentsLastHour > 3) score += 0.15m;
        if (signals.SameCardMultipleAccounts) score += 0.2m;
        if (signals.CountryMismatch) score += 0.1m;
        if (signals.HighValueBooking && signals.IsNewDevice) score += 0.1m;
        if (signals.LastMinuteBooking && signals.OneWayInternational) score += 0.1m;
        return Math.Min(score, 1.0m);
    }
}

Fraud Prevention Measures

MeasureImplementationImpact
Device fingerprintingFingerprintJS with server-side validationDetects account takeover
Velocity limitsRedis counters per user/card/IPPrevents card testing
3D Secure authenticationStripe 3DS2 / Adyen 3DS2Shifts liability to issuer
Address verification (AVS)Gateway AVS checkValidates billing address
CVC verificationGateway CVC checkValidates card in hand
ML fraud modelXGBoost trained on historical fraud85% fraud detection rate
Manual review queueSnowflake dashboard + ops teamCatches edge cases
Account lockout3 failed attempts = 24hr lockPrevents brute force

The ML fraud model is retrained weekly using labelled data from the manual review queue. Features include transaction velocity, device reputation, geographic distance between IP and card BIN country, time of day patterns, booking value relative to user history, and session behaviour signals (mouse movements, typing speed). The model achieves a precision of 92% and recall of 85%, with a false positive rate below 2% — critical for avoiding legitimate bookings being blocked.

20. Monitoring & Observability

A travel platform requires comprehensive observability across search latency, booking success rates, GDS partner health, payment gateway status, and business metrics like conversion rate and revenue per search. We use a three-pillar observability approach: metrics (Prometheus + Grafana), logs (ELK Stack), and traces (Jaeger). Automated alerting ensures the on-call team is notified within minutes of any degradation.

Key Metrics Dashboard

MetricTargetAlert ThresholdDashboard
Search latency (P95)< 500ms> 800ms for 5 minGrafana - Search
Booking success rate> 95%< 90% for 10 minGrafana - Bookings
Payment success rate> 98%< 95% for 5 minGrafana - Payments
GDS API availability> 99.9%< 99% for 10 minGrafana - Partners
Inventory hold expiry rate< 30%> 50% for 1 hourGrafana - Inventory
Error rate (5xx)< 0.1%> 0.5% for 5 minGrafana - Errors
Search-to-book conversion> 1.5%< 1.0% dailyGrafana - Business
Revenue per search> $0.50< $0.30 dailyGrafana - Revenue
Fraud detection rate> 85%< 70% weeklyGrafana - Security
Average booking value$450Drop > 20% week-over-weekGrafana - Business

Distributed Tracing Implementation

[ApiController]
[Route("api/v1/search")]
public class SearchController : ControllerBase
{
    private readonly ISearchService _searchService;
    private readonly ITracer _tracer;

    [HttpPost("flights")]
    public async Task<ActionResult<FlightSearchResponse>> SearchFlights(
        [FromBody] FlightSearchRequest request)
    {
        using var span = _tracer.BuildSpan("search.flights")
            .WithTag("origin", request.OriginCode)
            .WithTag("destination", request.DestinationCode)
            .WithTag("cabin_class", request.CabinClass?.ToString() ?? "ECONOMY")
            .Start();

        try
        {
            var results = await _searchService.SearchFlightsAsync(request);
            span.Log("results.count", results.Count);
            Metrics.SearchQueries.WithLabels(
                request.OriginCode, request.DestinationCode, "flight"
            ).Inc();
            Metrics.SearchResultsCount.Observe(results.Count);
            return Ok(results);
        }
        catch (Exception ex)
        {
            span.SetTag("error", true);
            span.Log("error.message", ex.Message);
            Metrics.SearchErrors.Inc();
            throw;
        }
    }
}

Alerting Rules

groups:
  - name: travel-platform
    rules:
      - alert: HighSearchLatency
        expr: histogram_quantile(0.95, search_latency_seconds_bucket) > 0.8
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Search P95 latency exceeds 800ms"

      - alert: BookingSuccessRateLow
        expr: rate(bookings_successful_total[5m]) / rate(bookings_attempted_total[5m]) < 0.90
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "Booking success rate dropped below 90%"

      - alert: PaymentGatewayDown
        expr: up{job="payment-gateway"} == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Payment gateway health check failing"

      - alert: InventoryHoldSpike
        expr: rate(inventory_hold_expired_total[5m]) > 50
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "High inventory hold expiry rate"

21. Cost Estimation

Running a production travel booking platform at scale involves significant infrastructure costs. Below is an estimated monthly cost breakdown for a system handling 10 million daily active users with 50 million searches per day.

Monthly Infrastructure Cost

ComponentSpecificationQuantityMonthly Cost
Application servers (ECS/EKS)c5.2xlarge (8 vCPU, 16 GB)20$5,600
PostgreSQL (RDS Multi-AZ)db.r5.2xlarge (8 vCPU, 64 GB)3 (1 primary + 2 replicas)$4,800
Elasticsearchr5.large.xsearch (2 vCPU, 16 GB)9 (3 shards x 3 nodes)$3,600
Redis (ElastiCache)r5.xlarge (4 vCPU, 26 GB)6 (cluster mode)$2,400
Kafka (MSK)kafka.m5.large6 brokers$2,160
ClickHouse (analytics)Storage optimised r5.2xlarge3$3,600
CDN (CloudFront)20 TB/month transfer1$1,700
Load balancers (ALB)Application load balancer2$400
S3 storage5 TB (itinerary PDFs, images)1$115
CloudWatch / MonitoringLogs + metrics1$800
KMS / SecretsEncryption keys1$50
DNS (Route 53)Hosted zone + queries1$50
Total Infrastructure$25,275

Third-Party Service Costs

ServiceUsageMonthly Cost
GDS access fees (Amadeus/Sabre)50M queries/month$50,000 - $150,000
Payment gateway fees2% of $225M GMV$4,500,000
Email (Amazon SES)5M emails/month$1,000
SMS (Twilio)1M SMS/month$70,000
Push notifications (FCM)10M push/month$0 (free tier)
Exchange rate API1M lookups/month$200
Total Third-Party$4,621,200
Note: GDS access fees are the single largest variable cost and vary dramatically by query volume and negotiated rates. Building direct connections with airlines and hotel chains can reduce GDS dependency by 30-50%, but requires significant upfront investment in API integration and certification. Payment gateway fees dominate the third-party costs — negotiate volume discounts aggressively and consider routing high-value transactions through gateways with lower percentage fees.

Cost Optimisation Strategies

  • Reserved instances: Purchase 1-year reserved instances for stable workloads (PostgreSQL, Redis, Kafka) to save 30-40% over on-demand pricing.
  • Spot instances for search: The Search Service is stateless and can run on spot instances for 60-70% savings, with auto-scaling to handle spot interruptions.
  • Cache optimisation: Increasing the Redis hit rate from 80% to 90% reduces Elasticsearch queries by 50%, potentially removing 3 nodes from the cluster.
  • Query batching: Batch multiple GDS queries into single API calls where the GDS supports it, reducing per-query costs by 40%.
  • Log retention tiers: Hot logs in ELK for 7 days, warm in S3 for 90 days, cold in Glacier for 7 years (regulatory requirement).

22. Testing Strategy

A travel platform must be tested rigorously across unit, integration, contract, and load testing dimensions. The unique challenge is that GDS partners have sandbox environments with limited availability and unpredictable responses. We build comprehensive mock servers that simulate GDS behaviour, including edge cases like overselling, timeout responses, and partial itinerary failures.

Testing Pyramid

Test TypeScopeToolCoverage Target
Unit testsPricing engine, fraud scoring, fare rulesxUnit + Moq90% line coverage
Integration testsDB queries, Redis, Kafka eventsTestcontainersAll repository methods
Contract testsGDS adapter responsesPactAll adapter interfaces
E2E testsFull search-to-booking flowPlaywright + APICritical user journeys
Load testsSearch and booking endpointsk6 / Gatling2x peak capacity
Chaos testsGDS failure, payment timeoutChaos Monkey / LitmusAll failure modes

Example Integration Test

public class BookingIntegrationTests : IAsyncLifetime
{
    private readonly PostgreSqlContainer _db;
    private readonly RedisContainer _redis;

    public async Task InitializeAsync()
    {
        _db = new PostgreSqlBuilder()
            .WithImage("postgres:16")
            .WithDatabase("travel_test")
            .Build();
        await _db.StartAsync();

        _redis = new RedisBuilder()
            .WithImage("redis:7")
            .Build();
        await _redis.StartAsync();
    }

    [Fact]
    public async Task FullBookingFlow_ShouldCreateConfirmedBooking()
    {
        var user = await CreateTestUserAsync();
        var searchResult = await SearchFlightsAsync("JFK", "LAX", DateTime.Now.AddDays(14));
        var selectedFlight = searchResult.Results.First();

        var priceLock = await LockPriceAsync(selectedFlight.Id, user.Id);

        var booking = await CreateBookingAsync(new CreateBookingRequest
        {
            SessionId = user.SessionId,
            PriceLockId = priceLock.Id,
            Legs = new[] { new LegRequest { InventoryId = selectedFlight.Id } },
            Passengers = new[] { new PassengerRequest { Name = "Test User" } },
            ContactInfo = new ContactInfo { Email = "test@example.com" }
        });

        var payment = await ProcessPaymentAsync(booking.BookingId, new PaymentRequest
        {
            Amount = priceLock.Amount,
            Currency = "USD",
            PaymentMethodId = CreateTestPaymentMethod()
        });

        booking.Should().NotBeNull();
        booking.Status.Should().Be(BookingStatus.CONFIRMED);
        booking.BookingRef.Should().StartWith("TRV");
        payment.Status.Should().Be(PaymentStatus.SUCCESS);

        var savedBooking = await GetBookingFromDbAsync(booking.BookingId);
        savedBooking.Should().NotBeNull();
        savedBooking.Legs.Should().HaveCount(1);
        savedBooking.Legs.First().SupplierConfirmation.Should().NotBeNullOrEmpty();
    }

    [Theory]
    [InlineData("JFK", "LAX", true)]
    [InlineData("DEL", "BOM", true)]
    [InlineData("LHR", "CDG", true)]
    public async Task SearchFlights_ShouldReturnResults(string origin, string dest, bool expected)
    {
        var results = await SearchFlightsAsync(origin, dest, DateTime.Now.AddDays(7));
        results.Results.Any().Should().Be(expected);
    }

    public async Task DisposeAsync()
    {
        await _db.DisposeAsync();
        await _redis.DisposeAsync();
    }
}

Load Testing Configuration

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
    stages: [
        { duration: '2m', target: 1000 },
        { duration: '5m', target: 5000 },
        { duration: '3m', target: 10000 },
        { duration: '2m', target: 1000 },
    ],
    thresholds: {
        http_req_duration: ['p(95)<500'],
        http_req_failed: ['rate<0.01'],
    },
};

export default function () {
    const payload = JSON.stringify({
        originCode: ['JFK', 'LAX', 'LHR', 'DEL', 'SIN'][Math.floor(Math.random() * 5)],
        destinationCode: ['LAX', 'JFK', 'CDG', 'BOM', 'NRT'][Math.floor(Math.random() * 5)],
        departureDate: new Date(Date.now() + 14 * 86400000).toISOString().split('T')[0],
        cabinClass: 'ECONOMY',
        passengers: { adults: 1 }
    });

    const res = http.post('https://api.example.com/api/v1/search/flights', payload, {
        headers: { 'Content-Type': 'application/json' },
    });

    check(res, {
        'status is 200': (r) => r.status === 200,
        'response time < 500ms': (r) => r.timings.duration < 500,
        'has results': (r) => JSON.parse(r.body).results.length > 0,
    });

    sleep(1);
}

23. Interview Q&A

Q1: How do you prevent overselling when two users book the same seat simultaneously?

Answer: We use a pessimistic locking strategy at the GDS level combined with a Redis-based distributed lock in our application layer. When a user selects a flight, we acquire a Redis lock with a 10-minute TTL using SETNX. If two users try to lock the same seat simultaneously, only one succeeds because Redis SETNX is atomic. We also request an inventory hold from the GDS, which provides a secondary guarantee. For hotels, we use optimistic concurrency control with version numbers — the booking UPDATE statement includes a WHERE clause checking the current version, and if it fails, we retry with a fresh read. The combination of application-level locks and GDS-level holds provides a two-layer defence against overselling.

Q2: How do you handle GDS API downtime or slow responses?

Answer: We implement a circuit breaker pattern (Polly in .NET) for each GDS adapter. If a GDS starts returning errors or responses exceed 5 seconds, the circuit opens for 60 seconds, during which searches are served from cached results or from other available GDS providers. We maintain at least 2 GDS connections (Amadeus + Sabre) and can degrade to cached results if both fail. The Inventory Service publishes a GDS health event to Kafka every 30 seconds, which the Search Service uses to exclude unhealthy providers from queries. During partial outages, search results are clearly marked with "prices may vary" banners to manage user expectations.

Q3: How would you design the dynamic pricing engine?

Answer: The pricing engine runs as a separate microservice that subscribes to booking events via Kafka. It maintains a demand model per route and date, updated in real-time as bookings occur. The model uses a weighted combination of five signals: historical booking velocity (30%), remaining inventory percentage (25%), search-to-book conversion ratio (20%), competitor prices (15%), and seasonal baseline (10%). The final multiplier ranges from 0.85x to 3.5x. Price locks are stored in Redis with TTL, and the pricing snapshot is persisted in ClickHouse for audit trail. The entire calculation completes in under 50ms because all inputs are pre-aggregated in Redis. We A/B test different weight configurations to optimise for revenue while maintaining competitive pricing.

Q4: How do you handle partial booking failures — for example, the first flight leg confirms but the second leg fails?

Answer: We implement the booking flow as a saga with compensating transactions. If the second leg fails after the first leg is confirmed, the compensating transaction cancels the first leg and refunds the payment. We also support partial bookings where the user can choose to keep the confirmed leg and get a refund for the failed leg. The saga state machine tracks each step, and the Booking Service can resume from any step after recovery. Failed compensations are logged and retried by a background job, ensuring eventual consistency. In practice, multi-leg failures are rare (less than 0.1% of bookings) because we validate all legs before initiating any confirmation.

Q5: How do you handle currency conversion and avoid arbitrage?

Answer: All internal prices are stored and computed in USD (the base currency). Conversion to display currencies happens at the API layer using exchange rates fetched from a reliable provider (e.g., Open Exchange Rates) and cached for 15 minutes in Redis. We apply a 1.5% forex markup to cover exchange rate fluctuation between display and capture. To prevent arbitrage, the payment amount is always charged in the card's native currency at the current rate with the markup — users cannot exploit stale exchange rates. If a user searches in EUR but pays with a USD card, the charge is in USD at the current rate. We also implement geo-velocity checks to detect users using VPNs to exploit regional pricing differences.

Q6: How would you scale the search service to handle 10,000 QPS during peak season?

Answer: The search service is stateless and horizontally scalable behind a load balancer. We target 20 instances of c5.2xlarge handling 500 QPS each. Elasticsearch is sharded across 9 nodes (3 shards, 1 replica each) with routing based on origin airport code to ensure related documents land on the same shard. Redis cluster with 6 nodes caches 80% of queries with a 60-second TTL. The CDN handles another 60% of static search page requests. We also implement search result precomputation for the top 500 origin-destination pairs, refreshing every 5 minutes, which absorbs about 30% of search traffic without hitting Elasticsearch at all. Auto-scaling policies add instances when CPU exceeds 70% or when queue depth exceeds 100.

Q7: How do you ensure payment idempotency when a user double-clicks the "Pay Now" button?

Answer: Every payment request includes a client-generated idempotency key derived from the booking ID and a timestamp. The Payment Service stores this key in Redis with a 24-hour TTL. If a duplicate request arrives with the same key, the stored response is returned without re-processing the payment. The Stripe/Adyen gateway also receives the same idempotency key, providing end-to-end protection. The booking state machine prevents any action on a booking that is already in a terminal state — if the booking is already CONFIRMED, a second payment attempt is rejected before reaching the payment gateway. The UI also disables the Pay button after the first click and shows a processing spinner.

Q8: How do you handle hotel overbooking (when the hotel runs out of rooms after our system confirms)?

Answer: Hotel overbooking is a real-world problem that our system must handle gracefully. When the hotel reports that a confirmed room is unavailable, the Hotel Operations team is notified immediately via PagerDuty. Our system automatically searches for an equivalent or better alternative at the same property or a nearby hotel. The rebooking covers any price difference if the alternative is more expensive. We maintain a "resort fund" — a pool of pre-negotiated emergency rates with nearby hotels — that can be activated within minutes. The guest receives a push notification, email, and SMS with the new accommodation details and a complimentary upgrade or discount as goodwill. Our SLA guarantees resolution within 2 hours of the overbooking being reported.

Q9: How do you implement feature flags for gradual rollout of new pricing algorithms?

Answer: We use LaunchDarkly (or an open-source alternative like Unleash) for feature flag management. When rolling out a new pricing algorithm, we create a flag like "pricing-engine-v2" and gradually increase the traffic percentage: 1% canary, then 10%, then 50%, then 100% over one week. During canary, we monitor key metrics: booking conversion rate, revenue per search, and customer complaints. If any metric degrades by more than 2%, we automatically roll back. The feature flag is evaluated at the Pricing Service level, and the chosen algorithm version is embedded in the price quote for audit trail purposes. We also run shadow mode testing where the new algorithm calculates prices in parallel without serving them, comparing results against the production algorithm.

Q10: Design a system to handle 1 million concurrent users during a flash sale.

Answer: For flash sales (e.g., Black Friday hotel deals), we implement several strategies: (1) A virtual waiting room (queue-it or custom) that holds users in a FIFO queue before they reach the search service, preventing thundering herd. (2) Pre-computed inventory snapshots loaded into Redis 5 minutes before the sale starts, so the search path never touches the database or GDS during the peak. (3) Static asset preloading via CDN with cache-busting at sale start time. (4) Rate limiting per user to 1 search per 5 seconds during the sale to reduce load. (5) Auto-scaling the search cluster to 50 instances (10x normal) 30 minutes before the sale. (6) A dedicated booking pipeline with reserved capacity that operates independently from the normal booking flow. (7) Real-time monitoring with a war room dashboard showing queue depth, search QPS, booking QPS, and inventory depletion rates.

Q11: How do you test GDS integrations without hitting production systems?

Answer: We build comprehensive mock servers that simulate GDS behaviour. Each mock implements the exact API contract of the real GDS (Amadeus, Sabre) including XML/JSON response formats, error codes, and latency profiles. The mocks are configured with test scenarios: normal availability, oversold flights, timeout responses, partial failures, and malformed data. We use Pact for contract testing to verify that our adapters correctly parse all expected response shapes. In CI/CD, integration tests run against mocks with chaos injection (random delays, error responses) to verify circuit breaker and retry logic. Pre-production environments connect to GDS sandbox environments for end-to-end validation before production deployment.

Q12: How would you design the review ranking algorithm to surface the most helpful reviews?

Answer: The ranking algorithm combines multiple signals into a composite score: recency (exponential decay with 6-month half-life), helpfulness (upvotes from other users, weighted by the voter's own credibility), verified booking status (2x boost for verified reviews), content quality (NLP analysis for detail length, specificity, and sentiment consistency with rating), and reviewer authority (based on number of helpful reviews and account age). The algorithm is periodically retrained using implicit feedback — we track which reviews users read before clicking "Book Now" and optimise the ranking to maximise booking conversion. We also implement diversity controls to ensure the top reviews represent different perspectives (not all 5-star or all 1-star).

Ayodhyya — System Design Blog Series

Design a Travel Booking System: The Complete Guide — Senior+ Guide