System Design Interview: Designing a High-Traffic Travel Booking System
Bài này có bản tiếng Việt. Đọc bản tiếng Việt →
Nguyen Hai Trinh • Senior Software Engineer @ SmileViet
The system design question I run into most in interviews is also one I hit every day as a backend engineer in travel: “Design a booking system (tours, flights, hotels) that survives a flash sale, where tens of thousands of people are all trying to grab the last few hundred spots.”
The algorithm in this one isn’t hard. What’s hard is a handful of gaps in how you think about it, and they only show up once you’ve run a system with real inventory constraints.
1. Clarify Requirements: ask before you draw
Before I draw a single box on the whiteboard, I ask three groups of questions.
- Business constraints: Is the seat or tour capacity a hard limit (say, 50 spots on one tour departure)? Is controlled overselling allowed, like in airlines, or is selling past capacity strictly off the table, like with a private tour?
- Experience requirements: When a user hits “Hold”, how long do they have to finish paying before the spot is released to someone else?
- Load: How many requests per second do we expect at the flash-sale peak? In a flash sale the share of successful requests is usually very low (most people simply “arrive late”), and that shapes the design.
Skipping this and jumping straight to “use Kafka, use Redis, use microservices” is shooting yourself in the foot. The interviewer hears right away that you’re assembling a ready-made recipe instead of thinking about the problem.
2. Back-of-envelope Estimation: do the numbers first
Say 50,000 people hit the page in the first 60 seconds of the sale, fighting over 500 spots.
- Peak QPS for the availability API: possibly thousands of requests per second. Reads outnumber writes by a wide margin.
- Peak QPS for the hold API (contended writes): much lower. But this is the sensitive part, because every request is trying to write to the same small set of data: 500 spots.
- Heavy reads can be handled by a cache and a CDN. The pain is in the writes: everyone piles onto one limited resource in a short window.
3. High-level Design: sketch the main flow
The main flow has three stages:
- Browse & Search: read-heavy, cached aggressively (CDN, Redis as a read cache). It doesn’t need strict consistency, since it only shows reference information.
- Hold/Lock the seat: the core of the system, and it needs strong consistency on the inventory.
- Payment & Confirmation: an asynchronous flow that can use a queue, with compensation if payment fails (release the seat).
The overall architecture is one straight path: Gateway, rate limiter, inventory service, hold service, payment queue, worker. Drawn out, it looks like this:
flowchart LR GW["Gateway"] --> RL["Rate limiter<br/>blocks bots and spam requests"] RL --> INV["Inventory service<br/>reads from cache"] INV --> LCK["Hold service<br/>writes to the source-of-truth store, with a lock"] LCK --> Q["Queue for async payment processing"] Q --> W["Worker<br/>confirm or roll back"]
4. Deep Dive: a seat lock with a TTL to prevent overselling
This is where I always spend the most time in an interview. It’s where someone who has run a real system and someone who has only studied the theory look most different.
The core of it: if many requests read the remaining seat count and then write a decrement, that’s read-then-write, and you have a race condition immediately. You will oversell. The fix is an atomic operation to hold the seat, with a TTL so the seat releases itself if the user doesn’t pay in time:
// Redis SETNX (set-if-not-exists) with a TTL to lock one specific seat
// Returns true if the hold succeeded, false if someone else already holds the seat
FUNCTION tryLockSeat(seatId, userId, ttlSeconds):
key = "seat_lock:" + seatId
success = REDIS.SET(key, userId, NX=true, EX=ttlSeconds)
RETURN success
A few points worth stating out loud when you present this:
- NX (not-exists) guarantees only one request wins the lock. Later requests fail immediately instead of waiting. The race condition is gone without needing a complex transaction at the database layer.
- TTL guarantees that if the user walks away (closes the tab, loses their connection), the seat is released automatically after a few minutes instead of being locked forever.
- Redis is only the temporary holder. The relational database (Postgres) remains the final source of truth once payment is confirmed, so a Redis restart doesn’t lose data.
5. Idempotency Key: stop double-booking when the client retries
This one gets missed a lot. When the network flakes during a flash sale, clients (web and app) will very often retry the hold or payment request on their own. If the server treats each retry as a new transaction, the customer gets the seat held twice, or gets charged twice.
The fix: every request that might be retried carries an Idempotency-Key generated by the client. The server stores the result of processing against that key. If the same key shows up again, it returns the earlier result instead of processing from scratch. Any API with a financial side effect needs this, not just booking.
Here’s one full retry cycle:
sequenceDiagram participant C as Client participant S as Server C->>S: POST hold seat (Idempotency-Key: K1) S->>S: Process, store result under K1 S-->>C: 200, hold succeeded Note over C,S: Network flakes, client never receives the response C->>S: Retry, same Idempotency-Key: K1 S-->>C: Return the previously processed result
6. Traffic over the limit: put users in a queue
When peak traffic exceeds what the hold service can process concurrently, adding more servers never finishes the job. It’s expensive and still capped. What we chose: put users into a virtual waiting queue.
- Hold requests are queued in first-come, first-served order.
- The system processes them at the rate the lock layer can actually handle, instead of letting everyone fight over it directly.
- The user experience: they see a “you’re in the queue, about X seconds left” screen instead of a 500 error or a timeout.
Past the threshold, the system degrades in a controlled way instead of falling over. In an interview this scores points because it shows you’re thinking about the real user experience, not just technical specs.
7. How to present it: Clarify, Estimate, High-level, Deep Dive
Whatever the question is, walk through all four steps (Clarify, Estimate, High-level Design, then Deep Dive) and the interviewer can follow your line of thinking. For the booking problem specifically, there are three keywords to bring up proactively: an atomic lock with a TTL, an idempotency key, and a controlled processing queue. Those three separate a purely theoretical answer from the answer of someone who has run a system with limited inventory in production.
Keep reading
- Frontend Lead Interview: Frontend System Design With Micro-Frontends, Monorepos and Trade-offsInterview & Career
- System Design Interview: Designing a Distributed Rate Limiter in GoInterview & Career
- Senior Fullstack Interview Playbook: Go Concurrency, System Design & STAR Behavioral AnswersInterview & Career
Need a developer for a contract or freelance project?
I take on fullstack projects — Go backends, React/Next.js frontends, or both ends. Send a few lines about the scope and timeline and I'll reply within 48 hours.