Building a Concurrency-Safe Movie Reservation System with FastAPI, Redis, and PostgreSQL
Introduction
One of the most interesting challenges in backend engineering isn't building CRUD APIs—it's designing systems that continue to behave correctly when hundreds of users interact with them simultaneously.
Consider this scenario:
A blockbuster movie opens for booking.
100 users click Reserve for the exact same seat at the exact same millisecond.
Without proper concurrency control, multiple users could end up paying for the same seat. Preventing this is one of the core responsibilities of a reservation system.
To better understand how production booking systems solve this problem, I built a Concurrency-Safe Movie Reservation Backend using FastAPI, PostgreSQL, Redis, and SQLAlchemy.
The goal wasn't to recreate BookMyShow or Ticketmaster. Instead, it was to explore the backend engineering concepts that make these systems reliable under concurrent traffic.
The Problem
At first glance, reserving a seat seems straightforward:
- Check if the seat is available.
- Create a reservation.
- Mark the seat as booked.
The problem appears when multiple requests arrive simultaneously.
Imagine two requests reaching the server at nearly the same time.
Request A
Check seat availability
Seat is free
Request B
Check seat availability
Seat is free
Request A
Reserve seat
Request B
Reserve seat
Both requests observed the seat as available before either completed the reservation.
This classic race condition can result in duplicate bookings if the system isn't designed carefully.
Designing the Reservation Workflow
To prevent this, I designed the reservation flow with multiple layers of protection rather than relying on a single mechanism.
The workflow looks like this:
User requests available seats
↓
Requested seats are temporarily locked in Redis
↓
Lock ownership is assigned
↓
Reservation request is submitted
↓
Lock ownership is verified
↓
Reservation is created inside a PostgreSQL transaction
↓
Reserved seats are persisted
↓
Redis locks are released
Each layer addresses a different failure scenario.
Layer 1: Distributed Seat Locking with Redis
The first challenge is preventing multiple users from selecting the same seat simultaneously.
Redis is ideal for this because it provides extremely fast in-memory operations.
Each seat lock is stored using a key similar to:
lock:showtime:{showtime_id}:{seat_label}
Every lock contains:
- User ID
- Expiration timestamp
These locks are temporary.
If a user abandons the booking process, the lock eventually expires, allowing someone else to reserve the seat.
Preventing Partial Seat Locking
Suppose a user wants three seats.
If the application locks seats individually, this situation could occur:
Seat A ✓
Seat B ✓
Seat C ✗
Now the user owns only part of the requested seats.
This creates unnecessary complexity.
Instead, I used Redis Lua scripts so the locking operation becomes atomic.
Either:
- every requested seat is locked
or
This eliminates inconsistent intermediate states.
Lock Ownership Verification
A Redis lock alone isn't enough.
Imagine this sequence:
- User A acquires a seat lock.
- The lock expires.
- User B acquires the same seat.
- User A submits an old reservation request.
Without ownership validation, the outdated request could incorrectly succeed.
Before creating a reservation, the application verifies that every requested seat is still locked by the same user.
If ownership has changed, the reservation is rejected.
Database Constraints as the Final Safety Net
Redis helps coordinate requests, but PostgreSQL remains the source of truth.
Even if every application-level protection somehow failed, the database still guarantees consistency.
A unique constraint ensures that a seat cannot be reserved twice for the same showtime.
This means duplicate reservations are impossible at the persistence layer.
Transactional Reservation Processing
Creating a reservation isn't a single database operation.
The application needs to:
- Create the reservation
- Insert reserved seats
- Update related records
- Persist everything consistently
If one operation fails midway, the database should not be left in a partially updated state.
For that reason, reservation creation happens inside a single PostgreSQL transaction.
Either:
or
This guarantees atomicity and consistency.
Idempotent Reservation Requests
Real users retry requests.
Browsers retry requests.
Mobile networks retry requests.
Without idempotency, duplicate retries may accidentally create multiple reservations.
To solve this, reservation requests include an Idempotency Key.
If the same request is received multiple times, the server returns the original result instead of creating another reservation.
This makes retries safe.
Protecting the API with Rate Limiting
Reservation endpoints are attractive targets for abuse.
To prevent excessive requests, I implemented Redis-based rate limiting using simple atomic operations:
INCR
EXPIRE
This throttles repeated requests while keeping the implementation lightweight.
Background Workers
Some operations shouldn't happen during the request-response cycle.
Background workers periodically:
- Release expired Redis locks
- Expire abandoned reservations
- Restore seat availability
This keeps reservation state consistent without blocking user requests.
Stress Testing the System
After implementing the concurrency protections, I wanted to verify that they actually worked.
Using Locust, I simulated:
- 100 concurrent users
- All attempting to reserve the exact same seat
- At nearly the same instant
The results were exactly what I hoped for.
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
- Redis lock expiration
- Database transaction failures
- Redis failure scenarios
These tests increased my confidence that the reservation workflow behaves correctly under concurrent load.
What I Learned
Building this project taught me much more than another CRUD application ever could.
I gained practical experience with:
- Race conditions
- Distributed locking
- Database transactions
- Concurrency control
- Idempotent APIs
- Background processing
- Redis coordination
- Transactional consistency
- Backend architecture
More importantly, it changed the way I think about backend development.
Correctness under concurrency is often more important than adding new features.
A system that behaves predictably under load is significantly harder—and more rewarding—to build.
Final Thoughts
This project was an opportunity to move beyond simple REST APIs and explore problems that real booking platforms face every day.
There are still many areas I'd like to improve, including:
- Optimistic locking strategies
- Distributed tracing
- Event-driven reservation workflows
- Horizontal scaling
- Kubernetes deployment
- Observability with Prometheus and Grafana
If you're interested in backend engineering or distributed systems, I'd love to hear how you would approach these challenges differently.
Constructive feedback and architectural discussions are always welcome.
Enjoyed this article? I also shared a shorter version on LinkedIn where I'm collecting architectural feedback from backend engineers. I'd love to hear your thoughts there as well.
LinkedIn Discussion: https://www.linkedin.com/posts/rahul-ch-434b1a250_backend-python-fastapi-ugcPost-7480908963971166208-KY2M/
Happy building! 🚀