Imagine this.
Tatkal booking opens at 10:00 AM.
At exactly 10:00:00, around 25 lakh people click the Book Now button.
There are only 4 seats left in your coach.
So the obvious question is:
How does the system decide which 4 people get the seats?
Is it literally first-come-first-served at the millisecond level?
And what happens to the other 24,99,996 requests?
This is a great system design problem because it teaches several important concepts at once:
- load balancing
- rate limiting
- stateless services
- race conditions
- atomic operations
- queues
- hot keys
- partitioning
- backpressure
- source of truth
Let’s build the system slowly, one problem at a time.
First, forget the database
A beginner might imagine the system like this:
25 lakh users
|
v
Database
That would be a disaster.
A real system usually has several layers:
User
|
v
Internet
|
v
Edge / CDN / WAF
|
v
Load Balancer
|
v
API Servers
|
v
Booking Service
|
v
Inventory Service
|
v
Database
Each layer exists because the simpler design eventually breaks.
Let’s understand why.
1. Who actually clicked first?
Suppose two users click:
User A clicks at 10:00:00.001
User B clicks at 10:00:00.009
It looks like User A should win.
But now include network latency.
User A network latency = 100 ms
User B network latency = 20 ms
Their requests reach the booking system at:
User A → 10:00:00.101
User B → 10:00:00.029
User B reaches the server first.
So:
The system usually cannot guarantee ordering based on the exact physical moment someone clicked.
The user's laptop is outside the system's control.
Why not send the click timestamp?
You might think the browser could send:
{
"clicked_at": "10:00:00.001"
}
But clients cannot be trusted.
A modified browser could simply send:
{
"clicked_at": "09:59:59.000"
}
So authoritative decisions should happen on the server.
This is a useful rule far beyond ticket booking:
Never trust the client for critical state.
Examples include:
Price
Permissions
Wallet balance
Inventory
Seat availability
Booking status
2. The first problem is not seats
Before worrying about the 4 seats, there is a more immediate problem:
25 lakh requests just arrived.
If all of them enter your backend, your servers may collapse before anyone gets a ticket.
So the first layer protects the system.
25 lakh users
|
v
+----------------------+
| Edge / Cloudflare |
|----------------------|
| Rate limiting |
| DDoS protection |
| Bot detection |
| Traffic filtering |
+----------+-----------+
|
v
Backend
This is called admission control.
3. Admission control: don't accept unlimited work
Imagine a nightclub with capacity for 500 people.
There are 20,000 people waiting outside.
You would not allow all 20,000 inside and then decide what to do.
There is a bouncer.
In distributed systems, the "bouncer" could be:
Rate limiting
Bot protection
Concurrency limits
Queues
Load shedding
The principle is:
Protect expensive downstream systems by limiting how much work enters.
This pattern appears everywhere:
- flash sales
- online exams
- ticket launches
- gaming events
- IPO applications
- payment systems
4. Then comes the load balancer
One server cannot handle millions of requests.
So we run many API servers.
Load Balancer
|
---------------------------
| | |
v v v
API-1 API-2 API-100
The load balancer spreads requests across them.
This is horizontal scaling.
Instead of buying one giant server, we add more servers.
If one server handles roughly:
5,000 requests/second
then 100 servers could theoretically handle around:
500,000 requests/second
Actual capacity depends on the workload, but the pattern is what matters.
5. API servers should be stateless
Here is an important mistake.
Suppose API Server 1 stores this in memory:
availableSeats = 4
And API Server 2 also stores:
availableSeats = 4
Now both servers might independently sell those four seats.
That is obviously wrong.
So critical shared state should not live independently inside each API server.
Instead:
API-1 -----\
API-2 ------\
API-3 -------> Shared Inventory Service
API-4 ------/
The API servers are mostly stateless.
This makes scaling easier because any request can go to any server.
If Server 27 crashes, the load balancer simply routes traffic elsewhere.
A useful mental model is:
Stateless compute is easy to scale. Shared mutable state is where distributed systems become difficult.
6. Now we reach the real problem: race conditions
Suppose only one seat remains.
availableSeats = 1
Two servers receive booking requests almost simultaneously.
Server A does:
READ availableSeats
It sees:
1
Server B also does:
READ availableSeats
It also sees:
1
Both then say:
seat is available
book it
Now one seat has been sold to two users.
This is a race condition.
The timeline looks like:
Time →
Server A READ seats = 1
Server B READ seats = 1
Server A WRITE seats = 0
Server B WRITE seats = 0
Both requests believed they succeeded.
7. The fix: atomic operations
The mistake above is that we did this:
READ
CHECK
WRITE
as separate steps.
Instead, the check and update should behave like one indivisible operation.
Conceptually:
IF seats > 0
THEN seats = seats - 1
One database implementation might look like:
UPDATE inventory
SET available_seats = available_seats - 1
WHERE train_id = ?
AND available_seats > 0;
Suppose one seat remains.
Request A executes first:
1 → 0
Success.
Request B executes next.
The condition:
available_seats > 0
is false.
So Request B fails.
No overselling.
This property is called atomicity.
Either the whole operation happens or none of it happens.
8. Great, so let 25 lakh requests hit this SQL query?
Not so fast.
The query may be logically correct, but there is another problem.
All 25 lakh users want the same resource.
25 lakh requests
|
v
+----------------------+
| Train 12952 |
| Tatkal 3A |
| available_seats = 4 |
+----------------------+
This single inventory record becomes extremely hot.
This is called a:
- hot row
- hot key
- hotspot
Even if you have 10,000 application servers, they may all eventually fight over the same inventory record.
10,000 API servers
|
v
same inventory row
This teaches an important lesson:
Scaling your application servers does not automatically scale shared state.
You see the same problem in:
- concert tickets
- Amazon flash sales
- limited sneaker drops
- coupon redemption
- wallet balances
- stock trading
9. Use a queue to absorb the burst
Instead of directly hammering the inventory system:
25 lakh requests
|
v
Inventory
we can place a queue in front:
25 lakh requests
|
v
+----------------+
| Booking Queue |
+-------+--------+
|
v
Workers
|
v
Inventory
Now the huge incoming burst can be absorbed temporarily.
Requests may enter the queue as:
R1
R2
R3
R4
R5
...
Suppose four seats exist.
R1 → seat → 3 left
R2 → seat → 2 left
R3 → seat → 1 left
R4 → seat → 0 left
R5 → sold out
The important transformation is:
Huge uncontrolled concurrency
becomes:
Controlled processing
10. Why queues are useful
Queues decouple two different rates:
Rate at which requests arrive
and:
Rate at which the backend can process requests
Without a queue:
Traffic spike
>>>>>>>>>>>>>>>>>>>>>>>> DATABASE
With a queue:
Traffic spike
>>>>>>>>>>>>>>>>>>>>>
|
v
+----------------------+
| Queue |
| ||||||||||||||||||| |
+----------+-----------+
|
v
Workers
----> ----> ----> Database
This is also a form of backpressure.
The downstream system says, in effect:
I will process work at the speed I can safely handle.
11. Should there be one global queue?
Probably not.
Imagine one queue for every booking in India.
Then a Delhi–Mumbai booking could block a completely unrelated Chennai–Bengaluru booking.
That wastes parallelism.
Instead, we partition the work.
A possible inventory key could be:
train_id + journey_date + class + quota
For example:
12952:2026-08-20:3A:TATKAL
All requests for the same inventory key should follow the same ordering path.
Different trains can be processed independently.
Train A → Partition 1 → Worker 1
Train B → Partition 2 → Worker 2
Train C → Partition 3 → Worker 3
This is partitioning or sharding.
12. The magic idea: serialize only conflicting work
This is one of the most reusable ideas in system design.
We do not need to serialize every booking in the country.
We only need to serialize requests that compete for the same inventory.
So instead of:
Every booking
|
v
one global worker
we do something closer to:
Same train/date/class/quota
|
v
ordered processing
while unrelated inventory runs in parallel.
A good interview phrase is:
Serialize operations per inventory key, not globally.
That gives us both:
Correctness
+
Parallelism
13. So which four people actually win?
Now we can finally answer the original question.
The system may create an authoritative order somewhere inside its infrastructure.
For example:
User B → sequence 91821
User X → sequence 91822
User A → sequence 91823
User Z → sequence 91824
User P → sequence 91825
There are four seats.
So:
91821 → gets seat
91822 → gets seat
91823 → gets seat
91824 → gets seat
91825 → sold out
The exact real-world click ordering may be impossible to know perfectly.
What matters is that the system defines one reliable ordering at a controlled point.
This leads to a broader distributed systems idea:
Sometimes we create an authoritative ordering instead of trying to discover the absolute real-world ordering.
14. What happens to the other 24,99,996 requests?
We ideally do not let every losing request perform an expensive database transaction.
Once the system confidently knows:
inventory = 0
future work can often be rejected earlier.
Instead of:
User
|
v
API
|
v
Queue
|
v
Worker
|
v
Database
|
v
Sold out
we may eventually do:
User
|
v
Booking Service
|
v
Sold out
This is called fail fast.
If an operation clearly cannot succeed, reject it as early and cheaply as possible.
This saves:
- CPU
- database connections
- queue capacity
- network calls
- locks
- memory
15. But be careful with caching
Suppose we cache:
Tatkal inventory = SOLD OUT
That is useful for fast rejection.
But what happens if someone fails payment?
The seat might become available again.
So we need to distinguish:
Cache
from:
Source of truth
A cache is fast, but it may be stale.
The authoritative inventory system decides the truth.
A good rule is:
Use caches to make the system faster, not to accidentally create a second source of truth.
16. Booking is not the same as confirmation
There is another important complication.
Imagine four users get seats, but payment takes two minutes.
Should those seats be permanently gone immediately?
Usually you need a temporary reservation.
The booking may move through states like:
AVAILABLE
|
v
HELD
|
v
PAYMENT_PENDING
|
+-----------+
| |
v v
CONFIRMED FAILED
|
v
AVAILABLE
Example:
4 seats
A → held
B → held
C → held
D → held
available = 0
Later:
A payment succeeds → confirmed
B payment fails → seat released
C succeeds → confirmed
D times out → seat released
This introduces several new system design topics:
- TTLs
- reservation expiry
- state machines
- retries
- payment failures
- compensation
These are natural follow-ups once the basic booking flow is correct.
17. Final high-level architecture
Putting everything together:
USERS
|
v
+-------------------+
| Edge / Cloudflare |
|-------------------|
| DDoS protection |
| Rate limiting |
| Bot detection |
+---------+---------+
|
v
+-------------------+
| Load Balancer |
+---------+---------+
|
+--------------+--------------+
| | |
v v v
API-1 API-2 API-N
\ | /
\ | /
+------------+------------+
|
v
+-------------------+
| Booking Service |
+---------+---------+
|
v
+-------------------+
| Booking Queue |
| partitioned by |
| inventory key |
+---------+---------+
|
+-----------+-----------+
| | |
v v v
Worker 1 Worker 2 Worker N
| | |
+-----------+-----------+
|
v
+-------------------+
| Inventory Service |
+---------+---------+
|
v
+-------------------+
| Database |
| Source of Truth |
+-------------------+
18. What did we actually learn?
The interesting part is not memorizing the architecture.
The important part is understanding why each component appeared.
| Problem | Pattern |
|---|---|
| Millions of incoming requests | Horizontal scaling |
| Backend may collapse | Admission control |
| Bots generate unfair traffic | Rate limiting / bot protection |
| Many API servers | Stateless services |
| Two users can get the same seat | Atomicity |
| Millions hit the same inventory | Hotspot recognition |
| Huge burst at 10 AM | Queue |
| Backend slower than incoming traffic | Backpressure |
| Independent trains should run separately | Partitioning |
| Same inventory needs ordering | Per-key serialization |
| Inventory already exhausted | Fail fast |
| Repeated reads are expensive | Caching |
| System needs one correct answer | Source of truth |
19. The most important system design habit
Do not start with:
We need Kafka.
We need Redis.
We need Cassandra.
Start with:
What problem do I have?
|
v
Why does it happen?
|
v
What is the simplest solution?
|
v
What breaks at scale?
|
v
What guarantee do I need?
|
v
Which system design pattern gives me that guarantee?
|
v
What new tradeoff did I introduce?
For example:
25 lakh requests
↓
backend overload
↓
admission control
Then:
traffic arrives faster than backend processes
↓
queue
Then:
multiple servers update same seat
↓
race condition
↓
atomic operation
Then:
everyone hits same inventory
↓
hotspot
↓
partition + serialize per key
That way, the architecture becomes a consequence of the problem instead of something you memorize.
20. The one sentence to remember
The hard part is not receiving 25 lakh requests. The hard part is safely coordinating millions of concurrent requests around a tiny amount of shared mutable state.
Once this idea makes sense, a lot of other systems start looking familiar:
- concert ticketing
- hotel bookings
- airline seats
- flash sales
- stock trades
- wallet balances
- coupon redemption
The domain changes.
The underlying system design patterns repeat.
Top comments (0)