A comprehensive proof-of-concept demonstrating different approaches to handling concurrency and race conditions in a ticketing system. This project explores various locking strategies to prevent double-booking scenarios when multiple users attempt to reserve the same seat simultaneously.
Based on: Building a Ticketing System: Concurrency, Locks, and Race Conditions
When thousands of users try to book the same concert ticket at exactly 10:00 AM, race conditions become a critical problem. This system demonstrates four different strategies to handle concurrent seat reservations:
- Naive Approach: Shows the problem with race conditions
- Pessimistic Locking: Uses database row-level locking with
SELECT FOR UPDATE - Optimistic Locking: Implements version-based conflict detection
- Redis Distributed Locking: Uses Redis for distributed coordination
- 🎯 Multiple Concurrency Strategies: Compare different approaches side-by-side
- ⚡ Load Testing: Built-in stress testing with 200 concurrent requests
- 🐳 Containerized Setup: PostgreSQL + Redis Cluster with Docker Compose
- 🔄 Race Condition Simulation: Artificial delays to trigger concurrent scenarios
- 📊 Performance Metrics: Success/failure rates and response time analysis
-
Clone and install dependencies
git clone <repository-url> cd poc-ticket-system bun install
-
Start the infrastructure
docker compose up -d
-
Run the application
bun run dev
-
Execute load tests
bun run test
-- Events table
CREATE TABLE events (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
-- Seats with optimistic locking version
CREATE TABLE seats (
id SERIAL PRIMARY KEY,
event_id INT NOT NULL REFERENCES events(id),
seat_number TEXT NOT NULL,
is_booked BOOLEAN NOT NULL DEFAULT false,
version INT NOT NULL DEFAULT 0 -- For optimistic locking
);
-- Booking records
CREATE TABLE bookings (
id SERIAL PRIMARY KEY,
event_id INT NOT NULL REFERENCES events(id),
seat_id INT NOT NULL REFERENCES seats(id),
user_id TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);Problem: Multiple requests can read the same seat as "available" simultaneously, leading to double bookings.
// Race condition: Both requests see seat as available
const seat = await client.query('SELECT is_booked FROM seats WHERE id = $1');
if (!seat.rows[0].is_booked) {
// Delay allows race condition to occur
await delay(randomMs);
await client.query('UPDATE seats SET is_booked = true WHERE id = $1');
}Solution: Lock the row immediately to prevent concurrent access.
// Lock the seat row - only one transaction can proceed
const seat = await client.query(
'SELECT id, is_booked FROM seats WHERE id = $1 FOR UPDATE'
);Pros: Guarantees no race conditions Cons: Lower concurrency, potential deadlocks
Solution: Use version numbers to detect concurrent modifications.
// Read current version
const seat = await client.query('SELECT version FROM seats WHERE id = $1');
const currentVersion = seat.rows[0].version;
// Update only if version hasn't changed
const result = await client.query(
'UPDATE seats SET is_booked = true, version = version + 1
WHERE id = $1 AND version = $2',
[seatId, currentVersion]
);
if (result.rowCount === 0) {
throw new ConcurrentModificationException();
}Pros: Better performance, no deadlocks Cons: Retry logic needed for conflicts
Solution: Use Redis for distributed locking across multiple service instances.
// Acquire distributed lock
const lockKey = `lock:booking:${seatNumber}`;
const acquired = await redisCluster.set(lockKey, uuid, {
NX: true, // Only set if not exists
PX: 500 // 500ms TTL
});
if (!acquired) {
throw new SeatNotAvailableException();
}Pros: Works across multiple servers, fault-tolerant Cons: Network dependency, complexity
POST /book?strategy={strategy_name}
Content-Type: application/json
{
"eventId": "1",
"seatNumber": "A1",
"userId": "user_123"
}Strategies: naive, pessimistic, optimistic, redis
Response:
{
"status": true
}POST /book/reset
Content-Type: application/json
{
"eventId": "1",
"seatNumber": "A1"
}The built-in load test simulates 200 concurrent users trying to book the same seat:
bun run testExample Output:
{
strategy: "naive",
durationMs: 54,
successCount: 10, # ❌ Multiple bookings (race condition)
failCount: 190,
reasons: {
HTTP_200: 10,
"Seat already booked": 190,
},
}
{
strategy: "pessimistic",
durationMs: 54,
successCount: 1, # ✅ Exactly one booking
failCount: 199,
reasons: {
HTTP_200: 1,
"Seat already booked": 199,
},
}
{
strategy: "optimistic",
durationMs: 50,
successCount: 1, # ✅ Exactly one booking
failCount: 199,
reasons: {
"Concurrent modification detected": 9,
HTTP_200: 1,
"Seat already booked": 190,
},
}
{
strategy: "redis",
durationMs: 25,
successCount: 1, # ✅ Exactly one booking + fastest!
failCount: 199,
reasons: {
HTTP_200: 1,
"Seat not available due to could not acquire lock": 199,
},
}- Race Conditions: Even simple operations can have complex concurrency issues
- Lock Granularity: Row-level vs table-level vs distributed locks
- CAP Theorem: Trade-offs between consistency, availability, and partition tolerance
- Performance vs Correctness: Different strategies for different requirements