MakeMyTrip – Hotel Booking System
0) Problem Restatement
Design a hotel booking platform like MakeMyTrip where users can search for hotels, view availability, book rooms, and make payments.
1) Requirements
1.1 Functional
- Search hotels by location, dates, and filters (price, rating, amenities).
- View hotel details, photos, reviews, and room availability.
- Book rooms with real-time availability checks.
- Payment processing and confirmation.
- Cancellation and refund handling.
- Hotel partner dashboard (manage inventory, pricing, bookings).
- User reviews and ratings.
1.2 Non-Functional
- High Availability: 99.9% uptime for search and booking.
- Low Latency: Search results < 500ms, booking confirmation < 2s.
- Consistency: No double-booking of rooms.
- Scalability: Handle millions of searches/day, peak traffic during holidays.
- Reliability: Accurate inventory sync across channels.
- Data Integrity: Secure payment and booking records.
1.3 Scale Estimates
- Hotels: 1 million hotels, avg 50 rooms/hotel = 50M rooms.
- Daily searches: 100M searches/day (~1200 searches/sec avg, 5000/sec peak).
- Daily bookings: 1M bookings/day (~12 bookings/sec avg, 50/sec peak).
- Users: 100M registered users.
- Data: Hotel metadata ~100 GB, booking records ~10 TB/year.
1.4) API Specifications
User-Facing APIs
- GET /api/hotels/search - Search hotels by location, dates, and filters
- GET /api/hotels/{hotel_id} - Get detailed hotel information including room types and amenities
- POST /api/hotels/availability - Check real-time room availability for specific dates
- POST /api/bookings - Create a new booking with room hold (returns booking_id and payment_url)
- GET /api/bookings/{booking_id} - Get booking details and status
- POST /api/bookings/{booking_id}/cancel - Cancel booking and initiate refund
Hotel Partner APIs
- PUT /api/partner/inventory - Update room inventory, pricing, and availability
- GET /api/partner/bookings - View bookings for partner's hotels
2) High-Level Architecture
2.1 Overview
- Client → API Gateway → Search Service → Inventory Service → Booking Service → Payment Service → Notification Service.
- Key components: Search with caching, inventory management with locking, async payment processing, and real-time availability updates.
2.2 Flow Diagram
Architectural Diagram Locked
3) Components (what & why)
Client (Web/Mobile)
- Search interface with filters (location, dates, price, rating, amenities).
- Hotel detail pages with photos, reviews, room types, and pricing.
- Booking flow with date selection, room selection, and payment.
API Gateway
- Authentication, rate limiting, request validation.
- Routes to appropriate microservices.
- SSL termination and DDoS protection.
Search Service
-
Responsibilities:
- Handle search queries with location, dates, and filters.
- Rank hotels by relevance, price, rating.
- Integrate with Inventory Service for real-time availability.
-
Optimization:
- Cache popular searches in Redis (location + dates as key).
- Use Elasticsearch for full-text search and filtering.
- Pre-aggregate data for common queries.
Inventory Service (Core)
-
Responsibilities:
- Manage room availability per hotel, room type, and date.
- Handle atomic room allocation (hold and confirm).
- Sync with hotel partner updates.
-
Data Structure:
Inventory(hotel_id, room_type, date, available_count, booked_count)
-
Locking Mechanism:
- Use Redis for temporary holds (TTL-based) while we wait for the booking to be confirmed.
- Use DB transactions for final booking confirmation.
Booking Service
-
Responsibilities:
- Orchestrate booking flow: hold room → payment → confirm.
- Create booking records with user, hotel, dates, and payment info.
- Handle cancellations and refunds.
-
State Machine: PENDING → PAYMENT_INITIATED → CONFIRMED / CANCELLED.
Payment Service
-
Responsibilities:
- Integrate with external payment gateways (Razorpay, Stripe, PayPal etc).
- Handle async callbacks, retries, and idempotency.
- Process refunds for cancellations.
-
Security: PCI-DSS compliance, tokenization, encrypted storage.
Hotel Metadata DB
- Store hotel info: name, location, amenities, photos, reviews, ratings.
- Read-heavy workload; use read replicas and caching.
Inventory DB
- Store room availability per hotel, room type, and date.
- High write load during bookings; use partitioning by hotel_id.
Booking DB
- Store booking records: user_id, hotel_id, room_type, dates, status, payment_id.
- Audit trail for all booking state changes.
Notification Service
- Asynchronous worker consuming from message queue.
- Send booking confirmations, reminders, and cancellation notifications via email/SMS/push.
Partner Dashboard
- Hotel partners manage inventory, pricing, and view bookings.
- Updates trigger cache invalidation for real-time consistency.
4) Data Model (minimal)
Hotel
sqlHotel(hotel_id, name, location, address, rating, amenities[], photos[], description)
Room Type
sqlRoomType(room_type_id, hotel_id, type_name, capacity, base_price, amenities[])
Inventory (per date)
sqlInventory(hotel_id, room_type_id, date, total_rooms, available_rooms, price)
Booking
sqlBooking( booking_id, user_id, hotel_id, room_type_id, check_in_date, check_out_date, num_rooms, total_price, status, -- PENDING, CONFIRMED, CANCELLED payment_id, created_at, updated_at )
User
sqlUser(user_id, name, email, phone, preferences[])
Review
sqlReview(review_id, user_id, hotel_id, rating, comment, created_at)
5) Key Flows
5.1 Search Flow
- User enters location (e.g., "Goa"), check-in/check-out dates, and filters.
- Search Service checks Redis cache for matching query.
- On cache miss: Query Elasticsearch for hotels matching location/filters.
- For each hotel, check availability via Inventory Service.
- Rank results by relevance, price, rating, and return to user.
- Cache results in Redis with TTL (5-10 minutes).
5.2 Booking Flow (Happy Path)
-
User selects hotel, room type, and dates.
-
Client calls Booking Service:
/book {hotel_id, room_type_id, dates, user_id}. -
Booking Service calls Inventory Service to hold rooms.
-
Inventory Service:
- Atomically decrements
available_roomsin DB using row-level lock. - Creates temporary hold in Redis with TTL (10 minutes).
- Returns hold_id to Booking Service.
- Atomically decrements
-
Booking Service initiates payment via Payment Service.
-
User completes payment on gateway.
-
Payment Gateway calls callback URL with success status.
-
Payment Service verifies callback and notifies Booking Service.
-
Booking Service confirms booking:
- Calls Inventory Service to finalize (remove hold, persist booking).
- Inserts booking record in Booking DB.
-
Emit booking confirmation event to message queue.
-
Notification Service sends confirmation email/SMS to user.
5.3 Failure Handling
- Payment Timeout: Hold TTL expires → auto-release rooms in Redis.
- Payment Failure: Mark booking as CANCELLED, release rooms.
- Double-Booking Prevention: Use DB row-level locking or optimistic concurrency control (version field).
5.4 Cancellation Flow
-
User requests cancellation → Booking Service validates cancellation policy.
-
If allowed:
- Update booking status to CANCELLED.
- Increment
available_roomsin Inventory DB. - Initiate refund via Payment Service.
- Invalidate search cache for affected dates.
6) Deep Dive A: Inventory Management & Overbooking Prevention (~10 mins)
Problem
Multiple users may attempt to book the last available room concurrently. We must prevent overbooking while maintaining high throughput.
Challenges
- Race Condition: Two users check availability (5 rooms available) → both book → oversell.
- Distributed Systems: Multiple Inventory Service instances need coordination.
- Performance: Locking should not bottleneck high traffic.
Solution: Two-Phase Booking with Locking
The key insight is to combine fast in-memory holds with persistent database confirmation to achieve both performance and correctness.
Phase 1: Temporary Hold (Redis)
-
When: User initiates booking (clicks "Book Now").
-
Action: Atomically decrement available_rooms in Redis using Lua script. Redis executes Lua scripts atomically - the entire script runs as a single operation without any other commands interleaving.
-
TTL: Hold expires in 10 minutes if payment not completed.
-
Benefit: Fast, in-memory operation; auto-releases on timeout.
-
Why Redis?:
- Ensures atomicity
- TTL automatically handles abandoned bookings (user closes browser, payment times out)
- Low latency (~1-2ms) for high throughput
Flow:
- User clicks "Book Now" → Booking Service calls Inventory Service
- Inventory Service executes Lua script on Redis (atomic check-and-decrement)
- If successful: Create hold record with TTL, return hold_id to user
- If failed: Return "Room unavailable" error
- User proceeds to payment page (has 10 minutes to complete)
Phase 2: Confirm Booking (DB)
- When: Payment succeeds (payment gateway callback received).
- Action: Persist booking in DB using transaction with row-level lock.
- Pessimistic Locking: FOR UPDATE ensures no other transaction can modify the same inventory row
Flow:
- Payment gateway sends success callback → Payment Service → Booking Service
- Booking Service starts DB transaction with FOR UPDATE (acquires row lock)
- Re-check availability in DB (defense against Redis failures or race conditions)
- If still available: Decrement DB count, insert booking record, release Redis hold
- If unavailable: Rollback, refund payment (rare edge case)
- Commit transaction (releases lock)
Why Two Phases?
- Phase 1 (Redis): Fast optimistic lock for user experience (no user wants to wait for DB during seat selection)
- Phase 2 (DB): Final pessimistic lock for correctness (ensures money and inventory are consistent)
- Separation of concerns: Redis handles ephemeral holds, DB handles permanent records
Two-Phase Booking Flow Diagram
Architectural Diagram Locked
SQL Example:
sqlBEGIN TRANSACTION; SELECT available_rooms FROM Inventory WHERE hotel_id = ? AND room_type_id = ? AND date = ? FOR UPDATE; -- Row-level lock UPDATE Inventory SET available_rooms = available_rooms - 1 WHERE hotel_id = ? AND room_type_id = ? AND date = ? AND available_rooms > 0; -- If update count = 0, rollback (race condition detected) COMMIT;
Lua Script for Atomic Hold (Redis)
lua-- Check if enough rooms available local available = redis.call('GET', 'inventory:' .. hotel_id .. ':' .. room_type .. ':' .. date) if tonumber(available) >= num_rooms then redis.call('DECRBY', 'inventory:' .. hotel_id .. ':' .. room_type .. ':' .. date, num_rooms) redis.call('SETEX', 'hold:' .. hold_id, 600, num_rooms) -- 10 min TTL return 1 -- Success else return 0 -- Insufficient rooms end
Alternative: Optimistic Concurrency Control
Instead of row-level locks, use a version number approach:
Step 1: Read current state
SELECT available_rooms, version FROM Inventory WHERE hotel_id = 'h123' AND room_type_id = 'r456' AND date = '2024-01-15'; --Returns: available_rooms = 5, version = 42
Step 2: Update with version check
UPDATE Inventory SET available_rooms = available_rooms - 1, version = version + 1 WHERE hotel_id = 'h123' AND room_type_id = 'r456' AND date = '2024-01-15' AND version = 42 -- Only succeed if version hasn't changed AND available_rooms > 0; --Check rows affected: --1 row affected → Success(you got the booking) --0 rows affected → Conflict(someone else modified it, retry)
Why it prevents race conditions:
- User A reads version = 42, tries to update WHERE version = 42 → SUCCESS(version now 43)
- User B reads version = 42, tries to update WHERE version = 42 → FAILS(version is now 43)
- User B retries with new version
Trade-offs:
- Pro: No locking overhead, better concurrency
- Con: Requires retry logic, can have high contention during peak times
- Best for: Low - conflict scenarios(OCC assumes conflicts are rare)
7) Deep Dive B: Search Optimization & Caching(~8 mins)
Problem
With 1200 searches / sec(peak 5000 / sec), querying the DB for every search is expensive and slow.
Solution: Multi - Layer Caching
Layer 1: Redis Cache(Hot Queries)
- ** Key **: `search:{location}:{check_in}:{check_out}:{filters_hash}`
- Value: List of hotel IDs with availability and pricing.
- TTL: 5-10 minutes (balance freshness vs cache hit rate).
- Cache Invalidation: On inventory update (hotel partner changes pricing/availability).
Layer 2: Elasticsearch (Full-Text Search)
- Purpose: Fast filtering by location, amenities, rating, price range.
- Indexing: Hotel metadata indexed with geo-coordinates.
- Query: Geo-proximity search + filters.
- Update: Near real-time indexing (1-2 sec delay acceptable).
Layer 3: Pre-Aggregation (Popular Routes)
- Strategy: For popular destinations (e.g., Goa, Dubai), pre-compute hotel lists for common date ranges.
- Storage: Store in cache with longer TTL (1 hour).
- Refresh: Periodic background job updates cache.
Caching Architecture
Architectural Diagram Locked
Search Ranking Algorithm
score = w1 * relevance_score // location match + w2 * (1 / price) // price preference + w3 * avg_rating // user reviews + w4 * availability_premium // more rooms = higher score + w5 * user_preference_match // personalization
Personalization
- Track user's past bookings, searches, and preferences.
- Boost hotels matching user's preferred amenities (e.g., pool, gym).
8) Deep Dive C: Payment & Idempotency (~7 mins)
Problem
Payment processing is async and may fail, timeout, or be retried. We must ensure exactly-once semantics.
Challenges
- Double Charging: User clicks "Pay" multiple times.
- Network Failure: Payment succeeds at gateway but callback fails.
- Retry Storm: Client retries on timeout, causing duplicate requests.
Solution: Idempotency Key
Implementation
- Client generates idempotency key (UUID) on first payment request.
- Payment Service checks if key exists in Idempotency Store (Redis/DB).
- If exists: Return cached response (no-op).
- If new: Process payment and store result with key.
- Store state machine per key:
- INITIATED → PROCESSING → SUCCESS → NOTIFIED / FAILED.
State Machine Diagram
Architectural Diagram Locked
Idempotency Store Schema
json{ "idempotency_key": "uuid-123", "booking_id": "booking-456", "status": "SUCCESS", "payment_id": "pay-789", "amount": 5000, "created_at": "2023-10-15T10:00:00Z", "response": { ... } // Cached response }
Webhook Signature Verification
- Payment gateway signs callback with secret key.
- Payment Service verifies signature to prevent spoofing.
Refund Handling
- On cancellation, create refund request with idempotency key.
- Track refund status separately: PENDING → PROCESSED.
9) Scaling & Performance (~5 mins)
Horizontal Scaling
- Search Service: Stateless; scale with load balancer.
- Inventory Service: Partition by hotel_id (consistent hashing).
- Booking Service: Stateless; scale horizontally.
- DB: Shard by hotel_id or geography (e.g., US-West, EU, Asia).
Database Partitioning
- Inventory DB: Shard by
(hotel_id, date)to distribute load. - Booking DB: Shard by
booking_idoruser_id. - Hot Partition Problem: Popular hotels (e.g., Taj Mahal Hotel) → use replica reads.
Caching Strategy
- Read-Heavy: Hotel metadata, reviews → Cache in CDN and Redis.
- Write-Heavy: Inventory updates → Write-through cache with invalidation.
CDN for Static Assets
- Hotel photos, videos → store in S3, serve via CloudFront.
Performance Metrics
- P99 Search Latency: < 500ms.
- P99 Booking Latency: < 2s.
- Cache Hit Rate: > 80% for searches.
10) Failure Modes & Recovery
Database Failure
- Read Replica Failover: Promote replica to master.
- Write Failures: Queue writes in Kafka, replay after recovery.
Redis Failure
- Search Cache: Degrade to Elasticsearch (slower but functional).
- Inventory Holds: Fallback to DB-only locking.
Payment Gateway Outage
- Queue Payments: Store in message queue, retry when gateway recovers.
- Fallback Gateway: Integrate multiple gateways (Stripe + PayPal).
Inventory Sync Issues
- Reconciliation Job: Nightly job compares Redis vs DB inventory counts.
- Audit Logs: Track all inventory changes for forensic analysis.
Geo-Redundancy
- Multi-region deployment (US, EU, Asia).
- DNS-based routing to nearest region.
11) Trade-offs & Alternatives
Eventually Consistent vs Strongly Consistent Inventory
- Strong: Use DB locks; slower but no overbooking.
- Eventual: Use Redis; faster but requires reconciliation.
- Choice: Hybrid (Redis for holds, DB for confirms).
SQL vs NoSQL
- SQL: ACID guarantees for bookings and payments.
- NoSQL: Better for hotel metadata (flexible schema).
- Choice: SQL for transactional data, NoSQL for metadata.
Synchronous vs Asynchronous Booking
- Sync: Immediate confirmation, better UX.
- Async: Decouple payment from booking, better resilience.
- Choice: Async with optimistic UI updates.
12) Security & Compliance
Payment Security
- PCI-DSS Compliance: No card data stored; use tokenization.
- TLS: All communication encrypted.
User Data Protection
- GDPR: User consent for data collection, right to deletion.
- Encryption: Encrypt PII (email, phone) at rest.
Rate Limiting
- Prevent scraping and DDoS attacks.
- Use API keys for partners.
Fraud Detection
- Monitor for unusual booking patterns (e.g., 100 bookings in 1 minute).
- Use CAPTCHA for suspicious activity.
13) Interview Time Allocation (45 min)
- 5 min: Requirements & scope (functional, non-functional, scale).
- 10 min: HLD & architecture diagram (components, data flow).
- 5 min: Data model & key flows (search, booking).
- 10 min: Deep dive on inventory management & overbooking prevention.
- 8 min: Deep dive on search optimization & caching.
- 5 min: Scaling, failure handling, and trade-offs.
- 2 min: Security, Q&A, and wrap-up.
14) Summary
- Core Challenges: Inventory consistency (overbooking prevention), search optimization (caching), payment reliability (idempotency).
- Key Components: Search Service (Elasticsearch + Redis), Inventory Service (Redis holds + DB confirms), Booking Service (orchestration), Payment Service (idempotency + async callbacks).
- Scaling Strategy: Horizontal scaling, DB partitioning by hotel_id, multi-layer caching (Redis + Elasticsearch + CDN).
- Correctness: Two-phase booking (Redis hold + DB confirm), row-level locking, idempotency keys for payments.
This design handles millions of searches and bookings per day while ensuring no overbooking and consistent user experience.
