Picture this: a national railway system runs a scheme where a small batch of tickets for last-minute, unplanned travel opens up for booking at an exact second, every single day. Not a rolling window. Not "book anytime in the next hour." One precise timestamp - say, 10:00:00 AM - and the moment it hits, every seat on every eligible train for that day becomes bookable.
Now scale the demand. Roughly 2 million people trying to book. Roughly 500 seats available on a popular route. Everyone hits "book" in the same second.
If this sounds like a niche problem specific to one country's railway system, it isn't. It's the exact same shape as:
Same problem, different costume: a fixed, tiny resource, and a demand spike synchronized to the millisecond. If you've built (or debugged) any of the above, you already know this pain in your bones.
This is Part 1 of a three-part deep dive. In this part, we set up the problem correctly and torch the naive approaches before they even get a chance to fail in production. Part 2 walks through the full architecture that actually survives this. Part 3 gets into the edge cases that separate "works in the demo" from "works on the worst day of the year."
Here's the instinct almost every engineer has the first time they hear this problem: just add more servers. More app instances, bigger database, more connections, a beefier load balancer. Scale horizontally, scale vertically, throw money at it.
This instinct is wrong, and it's worth being blunt about why.
Throughput scaling helps when the bottleneck is compute or bandwidth - things you can add more of. But the actual constraint here is the resource itself: there are 500 seats. Full stop. Adding a thousand more app servers doesn't create seat number 501. You could serve 2 million requests per second with infinite compute and you'd still have exactly 500 seats to hand out and 2 million people wanting one.
So the real engineering problem is not "how do we handle more load," it's "how do we let an enormous, synchronized crowd fairly and correctly compete for a resource that cannot grow." That reframing changes everything about the design. You're not building a system optimized purely for throughput. You're building a system optimized for correctness under contention, where fairness and zero double-booking matter more than raw requests-per-second.
Keep that distinction in your head, because it's the thing that makes every design decision downstream make sense.
Let's walk through the design almost every engineer reaches for first, and watch it fail in slow motion.
Naive attempt #1: Direct database check-then-update
async function bookSeat(userId: string, trainId: string): Promise<BookingResult> {
const availableSeats = await db.query(
"SELECT count FROM seat_inventory WHERE train_id = $1 FOR UPDATE",
[trainId]
);
if (availableSeats.count > 0) {
await db.query(
"UPDATE seat_inventory SET count = count - 1 WHERE train_id = $1",
[trainId]
);
await db.query(
"INSERT INTO bookings (user_id, train_id) VALUES ($1, $2)",
[userId, trainId]
);
return { success: true };
}
return { success: false, reason: "SOLD_OUT" };
}
This looks correct. FOR UPDATE takes a row lock, so no two transactions can read-and-decrement the same row simultaneously without waiting their turn. In isolation, on a whiteboard, this is textbook-correct.
Here's what actually happens at 2 million concurrent attempts against a handful of hot rows:
Naive attempt #2: "Let's just let everyone hit the API and see what happens"
No admission control, no waiting room, just raw traffic straight to your app tier. This fails even before it reaches the database. Your load balancer and app servers get flooded with connection attempts in the same second, most of which will fail or time out anyway since only 500 can succeed. You've built a system that spends 99.98% of its capacity serving people a "sorry, sold out" message as fast as possible, which is expensive busywork, not a feature.
Naive attempt #3: Optimistic locking with retries
Some engineers try to dodge lock contention with optimistic concurrency (check a version number, retry on conflict). This sounds better on paper, but at this scale it just moves the problem: instead of queueing on a lock, you get an enormous storm of failed writes and retries hammering the same row, which is arguably worse because now you're doing wasted write attempts instead of orderly waiting.
The common thread in all three failures: none of them address the actual shape of the problem. They're all trying to let 2 million requests directly contend for one shared piece of state, synchronously, in the hot path. That's the fundamental design flaw. The fix isn't a better lock. It's not letting 2 million requests fight over shared state directly in the first place.

That's where Part 2 picks up: structuring the traffic before it ever reaches your critical resource, so that by the time anything touches "the 500 seats," you're dealing with a controlled, ordered stream instead of an uncoordinated stampede.
Next up in Part 2: the actual architecture - waiting rooms, Redis-based atomic reservations, and how Kafka keeps the source of truth consistent without ever blocking the hot path.
Subscribe to get posts like this straight to your inbox - no noise, just quality content.