What happens when 100 users try to reserve the same seat at exactly the same time?
Without proper concurrency control, multiple users could end up booking the same seat—a classic race condition that every booking platform must solve.
To better understand how production reservation systems handle this problem, I built a Concurrency-Safe Movie Reservation Backend using FastAPI, PostgreSQL, Redis, and SQLAlchemy.
Rather than focusing on frontend features, my goal was to explore the backend engineering concepts that make reservation systems reliable under concurrent traffic.
Tech Stack
- FastAPI
- PostgreSQL
- SQLAlchemy
- Redis
- JWT Authentication
- Role-Based Access Control (RBAC)
- Docker
The Problem
Imagine a blockbuster movie just opened for booking.
One hundred users click Reserve for the same seat at almost the exact same moment.
A naïve implementation usually looks like this:
- Check whether the seat is available.
- Create the reservation.
- Mark the seat as booked.
It seems correct until multiple requests execute simultaneously.
Two requests can both read the seat as available before either has written the reservation.
The result?
❌ Duplicate bookings.
Preventing this became the primary goal of my project.
System Architecture
I organized the application using a layered architecture to keep responsibilities separated.
Client
↓
FastAPI Controller
↓
Service Layer
↓
Repository Layer
↓
PostgreSQL
↕
Redis
Each layer has a specific responsibility:
- Controller Layer handles validation, routing, and authentication.
- Service Layer contains the business logic.
- Repository Layer manages database operations.
- PostgreSQL stores the persistent data.
- Redis coordinates distributed locks and temporary state.
Keeping these responsibilities separate made the reservation workflow easier to reason about and test.
Preventing Double Booking
Instead of relying on a single protection mechanism, I used multiple layers.
1. Redis Seat Locking
When a user selects seats, the application first creates temporary locks in Redis.
Each lock follows this format:
lock:showtime:{showtime_id}:{seat_label}
Every lock stores:
- User ID
- Lock expiration timestamp
If the reservation isn't completed before the timeout, the lock expires automatically, allowing other users to reserve those seats.
This prevents multiple users from selecting the same seat simultaneously.
2. Atomic Multi-Seat Locking
Booking multiple seats introduces another challenge.
Suppose someone wants three seats.
If the application locks them one at a time, this could happen:
Seat A ✅
Seat B ✅
Seat C ❌
Now the user owns only part of the requested seats.
To avoid this inconsistent state, I implemented Redis Lua Scripts.
Either:
- every requested seat is locked
or
- none of them are.
This guarantees atomic seat acquisition.
3. Lock Ownership Verification
Temporary locks alone are not enough.
Imagine this scenario:
- User A acquires a seat lock.
- The lock expires.
- User B acquires the same seat.
- User A submits an old reservation request.
Without verifying lock ownership, User A could incorrectly reserve a seat that now belongs to User B.
Before creating a reservation, the application verifies that every requested lock still belongs to the requesting user.
If ownership has changed, the reservation fails safely.
4. Database Constraints
Redis helps coordinate concurrent requests.
PostgreSQL remains the single source of truth.
I added unique database constraints so that duplicate seat reservations are impossible even if every application-level safeguard failed.
This provides the final layer of protection against double booking.
5. Transactional Reservation Processing
Creating a reservation involves several database operations:
- Creating the reservation
- Saving reserved seats
- Updating related records
If one operation succeeds while another fails, the database could end up in an inconsistent state.
To prevent this, reservation creation runs inside a single PostgreSQL transaction.
Either:
- every operation succeeds
or
- the transaction rolls back completely.
This guarantees consistency.
Idempotent Reservation Requests
Real-world clients retry requests.
Users double-click buttons.
Browsers retry requests after network interruptions.
Without idempotency, duplicate requests could accidentally create multiple reservations.
To solve this, every reservation request includes an Idempotency Key.
If the same request is received again, the server simply returns the original response instead of creating another reservation.
Background Workers
Some tasks shouldn't happen during the request-response cycle.
Background workers periodically:
- Release expired Redis locks
- Expire abandoned reservations
- Restore seat availability
This keeps temporary reservation state synchronized without blocking incoming requests.
Rate Limiting
Reservation systems also need protection from abusive traffic.
I implemented Redis-based rate limiting using atomic Redis operations.
Protected endpoints include:
- Login
- Registration
- Seat Locking
- Reservation Creation
This prevents excessive requests while keeping the implementation lightweight.
Stress Testing the System
After implementing the concurrency protections, I wanted to verify that they actually worked.
Using Locust, I simulated the following scenario:
- 100 concurrent users
- All attempting to reserve the exact same seat
- At nearly the same moment
Results
| Metric | Result |
|---|---|
| Reservation Attempts | 100 |
| Successful Reservations | 1 |
| Failed Reservations | 99 |
| Duplicate Bookings | 0 |
Exactly one reservation succeeded.
Every competing request failed safely.
I also tested:
- Multiple users reserving different seats simultaneously
- Lock expiration and recovery
- Database transaction failures
- Redis failure scenarios
These tests gave me confidence that the reservation workflow behaves correctly under concurrent load.
What I Learned
This project taught me far more than building CRUD APIs.
Some of the biggest takeaways were:
- Concurrency bugs are much harder to reproduce than normal application bugs.
- Redis works best as a coordination layer—not as the source of truth.
- Database constraints remain essential, even when distributed locking is used.
- Transactions are critical for maintaining consistency.
- Correctness under concurrent traffic is often more important than adding new features.
Most backend tutorials stop after implementing CRUD operations.
Building a reservation system forced me to think about failure scenarios, race conditions, retries, and consistency—the kinds of problems production systems solve every day.
What's Next?
There are still many improvements I'd like to explore:
- Optimistic locking
- Event-driven reservation workflows
- Distributed tracing
- Horizontal scaling
- Kubernetes deployment
- Prometheus and Grafana for observability
Each of these would make the system even closer to a production-grade reservation platform.
Resources
📂 GitHub Repository
https://github.com/Rahul-2006/Movie-Reservation-Backend
💼 LinkedIn Discussion
I also shared this project on LinkedIn, where I'm collecting feedback from backend engineers on the architecture and concurrency strategy.
I'd genuinely appreciate your thoughts on the locking strategy, transaction flow, or any improvements you would suggest.
Top comments (0)