Skip to main content

Concurrency Control in Seat Booking: Complete Guide

· 8 min read
Anand Raja
Senior Software Engineer

When multiple users try to book the same seat simultaneously, we need concurrency control mechanisms to ensure only one user succeeds. Let me explain each approach step by step.

The Core Problem

Timeline of Concurrent Requests:
T1: User A checks seat C1 → Available ✅
T2: User B checks seat C1 → Available ✅
T3: User C checks seat C1 → Available ✅
T4: User A books seat C1 → Success
T5: User B books seat C1 → Success (PROBLEM!)
T6: User C books seat C1 → Success (PROBLEM!)

Result: Triple booking without concurrency control

1. Pessimistic Locking

What is it?

  • "Assume the worst will happen"
  • Lock the resource before accessing it
  • Other transactions wait until lock is released
  • Prevents conflicts by blocking concurrent access

Why called "Pessimistic"?

It pessimistically assumes that conflicts will occur, so it locks resources preemptively.

Step-by-Step Process:

Step 1: User A requests seat C1
Step 2: System acquires EXCLUSIVE LOCK on seat C1 row
Step 3: User B requests same seat → BLOCKED (waits)
Step 4: User C requests same seat → BLOCKED (waits)
Step 5: User A checks availability → Available
Step 6: User A books seat → Success
Step 7: System releases lock
Step 8: User B's request proceeds → Seat already taken → Fail
Step 9: User C's request proceeds → Seat already taken → Fail

Database Implementation:

PostgreSQL/MySQL:

BEGIN TRANSACTION;
SELECT * FROM seats WHERE id = 'C1' FOR UPDATE; -- Exclusive lock
-- Other transactions wait here
UPDATE seats SET booked = true WHERE id = 'C1';
COMMIT;

MongoDB (with transactions):

// Uses WiredTiger storage engine locking
session.withTransaction(async () => {
const seat = await Seat.findById('C1').session(session);
// MongoDB automatically locks the document
seat.isBooked = true;
await seat.save({ session });
});

Pros & Cons:

Pros: Guaranteed consistency, no race conditions
Cons: Can cause deadlocks, reduced concurrency, blocking


2. Optimistic Locking

What is it?

  • "Assume the best will happen"
  • Don't lock during read, check for conflicts before write
  • Uses version numbers or timestamps
  • Retry if conflict detected

Why called "Optimistic"?

It optimistically assumes conflicts are rare, so it doesn't lock upfront.

Step-by-Step Process:

Step 1: User A reads seat C1 (version: 5) → Available
Step 2: User B reads seat C1 (version: 5) → Available
Step 3: User C reads seat C1 (version: 5) → Available
Step 4: User A tries to update → Check version still 5? → YES → Success (version: 6)
Step 5: User B tries to update → Check version still 5? → NO (now 6) → CONFLICT → Retry
Step 6: User C tries to update → Check version still 5? → NO (now 6) → CONFLICT → Retry
Step 7: User B retries → Seat already booked → Fail
Step 8: User C retries → Seat already booked → Fail

Database Implementation:

MongoDB with Mongoose:

const seatSchema = new Schema({
seatNumber: String,
isBooked: Boolean,
version: { type: Number, default: 0 } // Version field
});

// Update with version check
const result = await Seat.updateOne(
{ _id: seatId, version: currentVersion, isBooked: false },
{ isBooked: true, $inc: { version: 1 } }
);

if (result.modifiedCount === 0) {
// Conflict detected - retry or fail
}

PostgreSQL/MySQL:

UPDATE seats 
SET booked = true, version = version + 1
WHERE id = 'C1' AND version = @current_version AND booked = false;

-- Check affected rows: 0 = conflict, 1 = success

Pros & Cons:

Pros: Better performance, no blocking, no deadlocks
Cons: Requires retry logic, can have starvation


3. Redis Distributed Locking

What is it?

  • Uses external system (Redis) for coordination
  • Creates distributed locks across multiple servers
  • Implements Redlock algorithm for reliability

Why Redis?

  • Fast: In-memory operations
  • Atomic: SET commands with expiration
  • Distributed: Works across multiple app instances

Step-by-Step Process:

Step 1: User A requests lock "seat:C1" → Success (gets unique token)
Step 2: User B requests same lock → Fails (lock exists)
Step 3: User C requests same lock → Fails (lock exists)
Step 4: User A performs booking logic → Success
Step 5: User A releases lock
Step 6: User B requests lock again → Success (lock available)
Step 7: User B checks seat → Already booked → Fail

Implementation Details:

// Acquire lock
const lockResult = await redis.set(
'lock:seat:C1', // Key
uniqueToken, // Value (UUID)
'PX', 30000, // Expire in 30 seconds
'NX' // Only if key doesn't exist
);

// Release lock (Lua script for atomicity)
const luaScript = `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`;

Pros & Cons:

Pros: Works across servers, fast, reliable
Cons: Additional infrastructure, network dependency


4. Atomic Operations

What is it?

  • Single, indivisible operations
  • Either completely succeeds or completely fails
  • No intermediate states visible to other operations

Why called "Atomic"?

Like an atom (indivisible), the operation cannot be broken into smaller parts.

Step-by-Step Process:

Step 1: User A, B, C all submit booking requests simultaneously
Step 2: Database processes one request at a time (internally)
Step 3: First request (User A) → findOneAndUpdate → Success
Step 4: Second request (User B) → findOneAndUpdate → No match (already booked) → Fail
Step 5: Third request (User C) → findOneAndUpdate → No match (already booked) → Fail

Database Implementation:

MongoDB:

// Atomic update - only succeeds if seat is available
const result = await Seat.findOneAndUpdate(
{ seatNumber: 'C1', isBooked: false }, // Condition
{ isBooked: true, bookedBy: userId }, // Update
{ new: true } // Return updated doc
);

// result is null if seat was already booked

PostgreSQL:

-- Single atomic operation
UPDATE seats
SET booked = true, booked_by = @user_id
WHERE seat_number = 'C1' AND booked = false
RETURNING *;

-- Returns empty if seat was already booked

Pros & Cons:

Pros: Simple, reliable, built-in database feature
Cons: Limited to single document/row operations


Real-World Platform Implementations

🚌 RedBus, MakeMyTrip (Bus/Travel Booking)

Primary Strategy: Atomic Operations + Optimistic Locking

// Simplified RedBus-like logic
async function bookSeat(userId, busId, seatNumber) {
// Atomic operation - most common approach
const result = await Seat.findOneAndUpdate(
{
busId: busId,
seatNumber: seatNumber,
status: 'available',
date: bookingDate
},
{
status: 'booked',
bookedBy: userId,
bookedAt: new Date(),
paymentPending: true // Hold for payment
},
{ new: true }
);

if (!result) {
return { success: false, message: "Seat not available" };
}

// Start payment timer (usually 10-15 minutes)
setTimeout(() => releaseSeatIfNotPaid(result._id), 15 * 60 * 1000);

return { success: true, seat: result };
}

Why This Approach?

  • Inventory is limited (40-50 seats per bus)
  • Lower concurrency compared to e-commerce
  • Payment hold mechanism - seat blocked during payment
  • Real-time updates to show seat availability

🛒 Flipkart, Amazon (Flash Sales)

Primary Strategy: Queue-Based + Inventory Reservation

// Simplified Flash Sale Logic
class FlashSaleService {
async addToCart(userId, productId, quantity) {
// Step 1: Queue the request
const queuePosition = await this.addToQueue(userId, productId, quantity);

if (queuePosition > this.getAvailableStock(productId)) {
return {
success: false,
message: "Out of stock",
waitlistPosition: queuePosition
};
}

// Step 2: Reserve inventory
const reserved = await this.reserveInventory(productId, quantity);

if (!reserved) {
return { success: false, message: "Stock exhausted" };
}

// Step 3: Add to cart with timer
await this.addToCartWithTimer(userId, productId, quantity);

return {
success: true,
message: "Added to cart",
timeToCheckout: "10 minutes"
};
}

async reserveInventory(productId, quantity) {
// Atomic decrement with check
const result = await Product.findOneAndUpdate(
{
_id: productId,
availableStock: { $gte: quantity }
},
{
$inc: {
availableStock: -quantity,
reservedStock: +quantity
}
}
);

return result !== null;
}
}

Why This Approach?

  • Massive concurrency (millions of users)
  • Large inventory (thousands of items)
  • Cart abandonment is common
  • Load balancing across multiple servers

🚂 IRCTC (Indian Railway)

Primary Strategy: Pessimistic Locking + Waitlist Queue

// Simplified IRCTC Logic
class IRCTCBookingService {
async bookTicket(userId, trainId, date, passengers) {
const session = await mongoose.startSession();

try {
await session.withTransaction(async () => {
// Pessimistic lock on train availability
const train = await Train.findOne({
trainId: trainId,
date: date
}).session(session);

if (train.availableSeats >= passengers.length) {
// Confirm booking
await this.confirmBooking(train, passengers, session);
} else if (train.waitlistSeats < train.maxWaitlist) {
// Add to waitlist
await this.addToWaitlist(train, passengers, session);
} else {
throw new Error("No seats available");
}
});
} finally {
await session.endSession();
}
}
}

Why This Approach?

  • Government system - reliability over performance
  • Complex rules (quotas, reservations, cancellations)
  • Waitlist management is critical
  • High data consistency requirements

Database-Specific Methods

MongoDB:

  • Primary: Atomic Operations (findOneAndUpdate)
  • Secondary: Optimistic Locking (version fields)
  • Advanced: Transactions with WiredTiger locking

PostgreSQL/MySQL:

  • Primary: Pessimistic Locking (FOR UPDATE)
  • Secondary: Optimistic Locking (version columns)
  • Advanced: Advisory locks, table-level locks

Redis:

  • Primary: Distributed locking (SET NX PX)
  • Advanced: Redlock algorithm for multiple Redis instances

Cassandra/DynamoDB:

  • Primary: Conditional updates (optimistic)
  • Secondary: Lightweight transactions

Choosing the Right Approach

For Small Applications:

Single Server + PostgreSQL/MySQL → Pessimistic Locking
Single Server + MongoDB → Atomic Operations

For Microservices:

Multiple Services → Redis Distributed Locking
High Throughput → Optimistic Locking + Retry Logic

For High Traffic:

Read-Heavy → Optimistic Locking
Write-Heavy → Queue-based system
Global Scale → Event sourcing + CQRS

Platform Comparison

PlatformPrimary StrategyConcurrency LevelKey Features
RedBusAtomic OperationsMedium (100s)Payment hold, Real-time updates
FlipkartQueue + Inventory ReservationVery High (Millions)Cart timer, Load balancing
IRCTCPessimistic LockingHigh (100K+)Waitlist, Quota management

Decision Matrix

ApproachConsistencyPerformanceComplexityBest For
Pessimistic⭐⭐⭐⭐⭐⭐⭐⭐⭐Traditional SQL apps
Optimistic⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐High concurrency
Redis Lock⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Microservices
Atomic Ops⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Simple use cases

Key Differences Explained

1. Inventory Nature

  • Bus/Flight: Fixed seats (40-50), binary availability
  • E-commerce: Large stock (1000s), quantity-based
  • Railway: Complex (quotas, classes, waitlist)

2. Business Model

  • Travel: Immediate booking + payment hold
  • E-commerce: Cart → Checkout flow
  • Railway: Booking → Waitlist → Confirmation

3. Technical Constraints

  • Travel: Real-time seat maps, payment integration
  • E-commerce: High throughput, global CDN
  • Railway: Government compliance, legacy systems

4. User Experience

  • Travel: "This seat is being booked by another user"
  • E-commerce: "Added to cart, 10 minutes to checkout"
  • Railway: "Waitlist position 25, likely to confirm"

Implementation Strategy Selection

Choose Atomic Operations When:

  • Limited inventory (< 100 items)
  • Medium concurrency (< 1000 simultaneous users)
  • Simple business rules

Choose Queue-Based When:

  • High concurrency (> 10K users)
  • Complex processing (payment, validation)
  • Flash sale scenarios

Choose Pessimistic Locking When:

  • Complex business rules
  • Government/Financial systems
  • Data consistency is critical

Choose Redis Distributed Locking When:

  • Microservices architecture
  • Multiple server instances
  • Cross-service coordination needed

The atomic operations approach is usually the best starting point because it's simple, reliable, and performs well for most seat booking scenarios. The choice depends on your specific requirements: scale, complexity, consistency needs, and **user experience