Full-stack cinema booking platform: seat reservation, LiqPay payments, refunds, and a bonus loyalty program, built to survive real backend failure modes — race conditions, unreliable payment callbacks, crashes mid-transaction. Java 21 / Spring Boot 4 / PostgreSQL / Redis / React 19 + TypeScript. Two-stage seat locking, idempotent payment callbacks, self-healing schedulers, full refund state machine, RBAC across 4 roles, 1000+ backend tests including dedicated concurrency suites.
Live demo — hosted on free tiers, so the first request may take up to a minute while the backend wakes up.
Full Documentation — complete feature descriptions, technical details, and project structure.
- Registration with email verification, JWT auth, Google OAuth2, password recovery
- Browse Now Showing / Coming Soon / Last Chance movies, custom availability calendar, session search
- Booking: seat selection with live availability, ticket-type pricing, bonus-point redemption, LiqPay payment, QR-coded tickets, confirmation email
- Refunds: time-based refund preview (100% / 85% / 50%) before confirming, automatic bonus rollback and seat release
- Profile management, ticket history (Active / Used / Refunded), bonus balance & transaction history
- Full CRUD for movies, genres, cast, halls (auto-generated seat layouts, interactive editor), schedule (conflict validation), promotions, ticket types
- User management: role changes, birth-date verification, block/unblock, per-user activity history
- Bookings and refunds lookup (payment details, LiqPay order IDs, refunds stuck in processing)
- Configurable bonus rules (welcome, birthday, booking spend, payment accrual); promotions award claimable bonus points
- Audit log of every admin change, with a per-entity history view
- Cashier: scanning a ticket's QR code opens its lookup/validation page at the door
Full feature breakdown for every role: docs/DOCS.md#features
Backend — Java 21, Spring Boot 4.1.1, Spring Security, Spring Data JPA, Hibernate 7, PostgreSQL 15, Flyway, Redis 7, JWT + Google OAuth2, MapStruct, Bucket4j (rate limiting), ZXing (QR codes), Brevo (transactional email), Testcontainers
Frontend — React 19 + TypeScript, Vite, React Router, Axios, CSS Modules, ESLint + Prettier
DevOps — Docker / Docker Compose, GitHub Actions CI, Render + Vercel + Neon + Upstash + Cloudinary deployment
Package by Feature + Layer: each business domain (booking/, payment/, refund/, bonus/, movie/, cinema/, user/, ticket/, promotion/, audit/) is a self-contained package with its own controller/service/repository/domain/dto/mapper, instead of one global layer shared by the whole app. notification/, integration/, and common/ are shared infrastructure packages (mail sending, file/QR handling, stateless utilities) — they never own a business decision, only get called by the domain that does. config/ and exception/ stay global across every domain.
flowchart TD
A[React Frontend] --> B[Spring Boot API]
B --> BK["booking/"]
B --> PM["payment/"]
B --> RF["refund/"]
B --> BN["bonus/"]
B --> MV["movie/ · cinema/ · ticket/ · user/ · ..."]
BK --> DB[(PostgreSQL)]
PM --> DB
RF --> DB
BN --> DB
MV --> DB
PM --> P[LiqPay API]
P --> CB[Callback Handler]
CB --> PM
RF --> P
S["Schedulers (per domain)"] --> DB
payment/ and refund/ depend on each other one way only (refund → payment → booking, never back), which keeps each domain safe to reason about on its own.
- Two-stage seat locking: 5-min pessimistic hold (
SELECT ... FOR UPDATE) on selection, then a 20-min reservation window before payment.@Versionoptimistic locking everywhere else conflicts are rare. No global locks — only individual seats, only temporarily. - Idempotent payment callbacks: conditional updates (
UPDATE ... WHERE status IN ('PENDING', 'PROCESSING')) make duplicate LiqPay callbacks safe to ignore;PaymentSchedulerreconciles payments stuck mid-flow. - Refund state machine:
PROCESSING → PROCESSED/REJECTED, with thePROCESSINGrow committed before the gateway call so a crash never loses a refund record;RefundSchedulerreconciles anything still stuck. - Scheduler-based self-healing: one scheduler per domain releases expired locks, cancels unpaid bookings, reconciles stuck refunds/payments, and recovers all of it from PostgreSQL state on restart — no in-memory state to lose.
Full write-up of trade-offs and what was learned building this: docs/DOCS.md
- Concurrency correctness: pessimistic row-level locks prevent double booking under concurrent requests for the same seat; verified with concurrency tests where exactly one of the racing requests succeeds.
- Idempotency everywhere it matters: payment callbacks, refund success/failure application, and bonus-point refunds are all safe to retry or receive duplicates without double-processing.
- Financial safety: refund amount/percentage/bonus math has a single source of truth (
RefundCalculator) shared by preview and execution, so they can never disagree; refund execution runs in its own transaction, committed before the external gateway call, so a mid-request crash can't leave a charge with no record. - RBAC across 4 roles (Admin, Content Manager, Cashier, User) enforced at both API and UI level, plus per-endpoint rate limiting against brute-force/abuse.
- 1128 tests across 150 test classes, run against real PostgreSQL via Testcontainers (no mocked DB in integration tests)
- ~91% line / 77% branch coverage (JaCoCo) — report generated by
./mvnw verifyatbackend/target/site/jacoco/index.htmland uploaded as a CI artifact - Dedicated concurrency test suites per domain:
SeatReservationConcurrencyTest,BookingConcurrencyTest,BookingDoubleConfirmConcurrencyTest,PaymentCallbackConcurrencyTest,RefundCreationConcurrencyTest,BonusCardConcurrencyTest,BonusRefundPointsRetryConcurrencyTest,TicketValidationConcurrencyTest - Failure scenarios verified directly: concurrent holds on the same seat (exactly one wins), duplicate concurrent LiqPay success callbacks (payment reaches
SUCCESSand tickets are issued exactly once), expired reservations auto-released by the scheduler, refunds stuck inPROCESSINGreconciled - k6 load test: 2,000 concurrent hold requests from 200 users racing for 10 seats → exactly 10 winners, 1,990 clean
409s, 0 server errors, 0 double bookings verified in the database — see load-test/ - Runs on every push/PR to
master/developvia GitHub Actions (.github/workflows/ci.yml)
cd backend && ./mvnw testgit clone https://github.com/AntonBas/Cinema.git
cd Cinema
cp .env.docker.example .env # fill in JWT_SECRET, LiqPay sandbox keys, Brevo and Google OAuth credentials
docker compose up -d| Service | URL |
|---|---|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:8080/api |
| Swagger | http://localhost:8080/swagger-ui.html |
Seeded test accounts (admin / content manager / cashier / user), local non-Docker setup, and database reset instructions: docs/DOCS.md#getting-started
Cloud deployment (Render + Vercel + Neon + Upstash + Cloudinary, free tier): docs/DOCS.md#cloud-deployment-free-tier

