Real-time cinema seat booking backend β built with Go, Redis, and WebSockets.
FirstClick is a lightweight cinema seat-booking backend that handles concurrent seat holds with Redis atomic locks, real-time seat-grid updates over WebSockets, and a clean REST API consumed by a static frontend.
Browser ββRESTβββΆ Gin API βββΆ BookingService βββΆ RedisStore
β² β
βββββββββββββWebSocket (SEATS_UPDATED broadcast)βββββββββββ
- β‘ Atomic seat locking via Redis
SETNXβ only one user wins per seat - π Service-level mutex for consistent booking behavior
- β±οΈ Auto-expiring holds β seats release automatically via Redis TTL (20s)
- π‘ WebSocket broadcast β all clients get instant seat-grid updates
- π§± Clean layered architecture β service / store / model separation
firstclick/
βββ cmd/
β βββ firstclick/
β βββ main.go # Server entrypoint, Gin router, routes
βββ internal/
β βββ service/
β β βββ service.go # BookingService β hold / confirm / release logic
β βββ store/
β β βββ memory_store.go # MemoryStore + RedisStore (Redis atomic seat lock)
β βββ model/
β β βββ model.go # Booking struct, SeatStatus enum
β βββ redis/
β β βββ redis.go # Redis client connection helper
β βββ realtime/
β βββ seats_hub.go # WebSocket hub for SEATS_UPDATED broadcasts
βββ static/
βββ index.html # Single-page frontend (REST + WebSocket client)
All endpoints are prefixed at http://localhost:8080.
| Method | Path | Description |
|---|---|---|
GET |
/movies |
List all movies |
GET |
/movies/:movieId/seats |
Get live seat statuses for a movie |
POST /movies/:movieId/seats/:seatId/hold
PUT /sessions/:sessionId/confirm
DELETE /sessions/:sessionId
Hold a seat for 20 seconds.
// Request
{ "user_id": "abc123" }
// Response 200
{
"session_id": "uuid",
"movie_id": "movie-1",
"seat_id": "B4",
"expires_at": 1712000000000
}Confirm a held seat (must be called before TTL expires).
// Request
{ "user_id": "abc123" }
// Response 204 No ContentRelease a held seat early.
// Request
{ "user_id": "abc123" }
// Response 204 No Content| Path | Serves |
|---|---|
GET / |
static/index.html |
GET /index.html |
static/index.html |
GET /static/* |
Assets from static/ |
Endpoint: GET /ws/seats
The server broadcasts a message whenever any seat state changes:
{
"type": "SEATS_UPDATED",
"movie_id": "movie-1",
"ts": 1712000000000
}The browser listens and re-fetches /movies/:movieId/seats immediately when movie_id matches the currently selected movie β keeping all open tabs in sync without polling.
| Value | Meaning |
|---|---|
available |
Free to hold |
hold |
Temporarily locked (TTL: 20s) |
confirmed |
Permanently booked |
booked |
Not used by current flow (kept for future states) |
reserved |
Not used by current flow (kept for future states) |
type Booking struct {
MovieID string
SeatID string
UserID string
Status SeatStatus
ExpiresAt time.Time // populated on hold, used by UI countdown timer
}Two layers protect against double-booking:
Request 1: Hold B4 βββΆ SETNX seat:movie-1:B4 βββΆ β
wins (key didn't exist)
Request 2: Hold B4 βββΆ SETNX seat:movie-1:B4 βββΆ β key exists β error returned
Layer 1 β Redis SETNX (primary gate)
- Key:
seat:<movie_id>:<seat_id> - Value: JSON-encoded
BookingwithStatus = hold - TTL: 20 seconds β seat auto-releases if user doesn't confirm
Layer 2 β sync.Mutex in BookingService (consistency guard)
- Ensures the read-check-write sequence inside the service stays coherent
- Redis remains the actual source of truth
Note: If you ever swap the store to an in-memory
map, the mutex becomes critical for preventing data races. With Redis it's a belt-and-suspenders guard.
- Go 1.21+
- Redis 7.x running locally
# Start Redis (if not already running)
redis-server
# Run the server
make run
# Open in browser
open http://localhost:8080| Variable | Default | Description |
|---|---|---|
REDIS_ADDR |
localhost:6379 |
Redis server address |
# Custom Redis address
REDIS_ADDR=redis.internal:6379 make run- Multi-seat selection β bulk hold and bulk confirm in a single session
- Database persistence β add PostgreSQL as a durable backing store alongside Redis
- Write-aside caching β use Redis as a read cache with DB as source of truth for confirmed bookings
- Payment integration β hook confirm step into a payment gateway
- Admin dashboard β manage movies, view occupancy, force-release holds
- Fork the repository
- Create a feature branch:
git checkout -b feat/your-feature - Commit your changes:
git commit -m 'feat: add bulk seat hold' - Push and open a pull request