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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
It looks like User A should win.
But now include network latency.
User A network latency = 100 ms
User B network latency = 20 ms
Enter fullscreen mode Exit fullscreen mode
Their requests reach the booking system at:
User A → 10:00:00.101
User B → 10:00:00.029
Enter fullscreen mode Exit fullscreen mode
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"
}
Enter fullscreen mode Exit fullscreen mode
But clients cannot be trusted.
A modified browser could simply send:
{
"clicked_at": "09:59:59.000"
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
then 100 servers could theoretically handle around:
500,000 requests/second
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
And API Server 2 also stores:
availableSeats = 4
Enter fullscreen mode Exit fullscreen mode
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 ------/
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
Two servers receive booking requests almost simultaneously.
Server A does:
READ availableSeats
Enter fullscreen mode Exit fullscreen mode
It sees:
1
Enter fullscreen mode Exit fullscreen mode
Server B also does:
READ availableSeats
Enter fullscreen mode Exit fullscreen mode
It also sees:
1
Enter fullscreen mode Exit fullscreen mode
Both then say:
seat is available
book it
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
Both requests believed they succeeded.
7. The fix: atomic operations
The mistake above is that we did this:
READ
CHECK
WRITE
Enter fullscreen mode Exit fullscreen mode
as separate steps.
Instead, the check and update should behave like one indivisible operation.
Conceptually:
IF seats > 0
THEN seats = seats - 1
Enter fullscreen mode Exit fullscreen mode
One database implementation might look like:
UPDATE inventory
SET available_seats = available_seats - 1
WHERE train_id = ?
AND available_seats > 0;
Enter fullscreen mode Exit fullscreen mode
Suppose one seat remains.
Request A executes first:
1 → 0
Enter fullscreen mode Exit fullscreen mode
Success.
Request B executes next.
The condition:
available_seats > 0
Enter fullscreen mode Exit fullscreen mode
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 |
+----------------------+
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
we can place a queue in front:
25 lakh requests
|
v
+----------------+
| Booking Queue |
+-------+--------+
|
v
Workers
|
v
Inventory
Enter fullscreen mode Exit fullscreen mode
Now the huge incoming burst can be absorbed temporarily.
Requests may enter the queue as:
R1
R2
R3
R4
R5
...
Enter fullscreen mode Exit fullscreen mode
Suppose four seats exist.
R1 → seat → 3 left
R2 → seat → 2 left
R3 → seat → 1 left
R4 → seat → 0 left
R5 → sold out
Enter fullscreen mode Exit fullscreen mode
The important transformation is:
Huge uncontrolled concurrency
Enter fullscreen mode Exit fullscreen mode
becomes:
Controlled processing
Enter fullscreen mode Exit fullscreen mode
10. Why queues are useful
Queues decouple two different rates:
Rate at which requests arrive
Enter fullscreen mode Exit fullscreen mode
and:
Rate at which the backend can process requests
Enter fullscreen mode Exit fullscreen mode
Without a queue:
Traffic spike
>>>>>>>>>>>>>>>>>>>>>>>> DATABASE
Enter fullscreen mode Exit fullscreen mode
With a queue:
Traffic spike
>>>>>>>>>>>>>>>>>>>>>
|
v
+----------------------+
| Queue |
| ||||||||||||||||||| |
+----------+-----------+
|
v
Workers
----> ----> ----> Database
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
For example:
12952:2026-08-20:3A:TATKAL
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
we do something closer to:
Same train/date/class/quota
|
v
ordered processing
Enter fullscreen mode Exit fullscreen mode
while unrelated inventory runs in parallel.
A good interview phrase is:
Serialize operations per inventory key, not globally.
That gives us both:
Correctness
+
Parallelism
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
There are four seats.
So:
91821 → gets seat
91822 → gets seat
91823 → gets seat
91824 → gets seat
91825 → sold out
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
future work can often be rejected earlier.
Instead of:
User
|
v
API
|
v
Queue
|
v
Worker
|
v
Database
|
v
Sold out
Enter fullscreen mode Exit fullscreen mode
we may eventually do:
User
|
v
Booking Service
|
v
Sold out
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
from:
Source of truth
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
Example:
4 seats
A → held
B → held
C → held
D → held
available = 0
Enter fullscreen mode Exit fullscreen mode
Later:
A payment succeeds → confirmed
B payment fails → seat released
C succeeds → confirmed
D times out → seat released
Enter fullscreen mode Exit fullscreen mode
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 |
+-------------------+
Enter fullscreen mode Exit fullscreen mode
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 truth19. The most important system design habit
Do not start with:
We need Kafka.
We need Redis.
We need Cassandra.
Enter fullscreen mode Exit fullscreen mode
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?
Enter fullscreen mode Exit fullscreen mode
For example:
25 lakh requests
↓
backend overload
↓
admission control
Enter fullscreen mode Exit fullscreen mode
Then:
traffic arrives faster than backend processes
↓
queue
Enter fullscreen mode Exit fullscreen mode
Then:
multiple servers update same seat
↓
race condition
↓
atomic operation
Enter fullscreen mode Exit fullscreen mode
Then:
everyone hits same inventory
↓
hotspot
↓
partition + serialize per key
Enter fullscreen mode Exit fullscreen mode
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.
답글 남기기
댓글을 달기 위해서는 로그인해야합니다.