---
title: "BookMyMovie: Building a Cinema Ticketing Platform"
description: "The story of BookMyMovie, a cinema booking site I built alone: why buying a ticket in Pakistan is still a hassle, the one rule the whole system is built around, how two people can never end up with the same seat, the back office, the design, and the bugs that slipped through."
author: "Syed Ahmer Shah"
date: 2026-09-29
url: https://ahmershah.dev/blogs/bookmymovie-case-study-cinema-ticketing-platform
tags: ["bookmymovie", "case-study", "laravel", "react", "mysql", "system-design", "software-architecture", "web-security"]
series: "The Engineering Logs" (part 9)
---

# BookMyMovie: Building a Cinema Ticketing Platform

_A story about selling cinema seats online without ever selling the same seat twice, and about everything that grew around that one promise_

![BookMyMovie: Building a Cinema Ticketing Platform — Ahmer at a laptop in a cinema lobby beside the BookMyMovie app on a phone, with popcorn and a Now Showing wall behind](https://ahmershah.dev/blog/bookmymovie-case-study-cinema-ticketing-platform/cover-fbfa49ce.webp)


Picture a Friday night. You and three friends want to see the new film at 9 PM. One of you phones the cinema and gets no answer. Another tries a booking site, picks four seats together in row G, types in a card number, and at the very last step the site says two of those seats are already gone. So you start again, the good seats are taken, and somebody ends up in the front row with a sore neck.

That small, annoying evening is why BookMyMovie exists.

I started it in July 2026 as my Aptech DISM project, and it grew into a full cinema platform that I designed and built on my own. That covers the research, the screens, the database, the server, payments, security, the search engine work, the back office for cinema staff, the tests and the documentation. You browse films, watch trailers, choose your exact seat on a live map (or sit in it first, inside a 3D model of the hall), add snacks, and get a QR ticket on your phone that still opens when the lobby has no signal.

This is the story of how it came together, what went wrong along the way, and what I would do differently.

* * *

## One promise

Before I drew a single screen, I wrote one sentence on a sticky note:

"Never sell the same seat twice, and never show a price you won't charge."

A shop can apologise for a late parcel. It can't really apologise for two strangers standing in the dark, both holding a ticket for seat G7. Nearly every decision in this project traces back to that note.

![BookMyMovie's home page: a trailer plays silently behind the featured film, with Book tickets, Watch trailer and Film details, and the next films in the rotation along the bottom.](/blog/bookmymovie-case-study-cinema-ticketing-platform/home.webp)

## The people it's for

I kept three kinds of people in mind the whole time.

The first is the moviegoer. They want to see what's on, watch a trailer, compare a few films, pick good seats, and know the price before they commit. Some of them are bringing children, some want to split the bill with friends, some are happy to wait for a cancellation on a sold-out show, and some would rather read the whole site in Urdu. Every one of those wishes became a feature: adult and child tickets in one booking, a "split the bill" link for each friend, a waitlist that tells you when a seat comes back, and a full Urdu version that reads right to left.

The second is the person at the box office. Their night is about speed: scan a ticket at the door, look up a booking, issue a refund, sell a gift card.

The third is the manager or owner, who decides which films play when, sets the prices, runs offers, and wants to know at a glance whether tonight is going well.

![A film page: poster, rating and runtime, with Choose a showtime, Add to watchlist, Watch trailer and Compare, and the film's trailer playing behind it.](/blog/bookmymovie-case-study-cinema-ticketing-platform/film.webp)

## Choosing the tools

I wanted the site to feel like an app, where tapping a link doesn't blank the screen and reload everything. I also wanted the rules (who is signed in, what they're allowed to see, whether a seat is free) to live on the server, where I can trust them and test them.

Laravel 12 on the server and React 19 in the browser, joined together by a small library called Inertia, gave me both. If you're not a developer, think of it this way: the server is the cinema manager who checks every request, and React is the friendly usher who shows you around without making you leave the building each time you move. Because the page frame never reloads, your seat-hold countdown and the little assistant in the corner stay put as you move around.

| Layer | What I used | Why |
|---|---|---|
| Server | Laravel 12, PHP 8.2 | Routing, accounts, background jobs, rate limits, e-mail |
| The bridge | Inertia 2 + Ziggy | App-like navigation while the server keeps control |
| What you see | React 19, strict TypeScript, Tailwind CSS 4 | Screens, and one set of colours for two themes |
| Movement and 3D | Motion, Lenis, Three.js | Page animation, smooth scrolling, the poster ring and the 3D hall |
| Data | MySQL 8 / MariaDB | The database itself refuses impossible bookings |
| Payments | JazzCash, Easypaisa, card, pay at the counter | What people in Pakistan actually use |
| E-mail and jobs | Resend and a job queue | Sending an e-mail never slows a page down |

## How the pieces fit

![How a request flows: React pages in the browser, Laravel middleware, controllers and support services, then MySQL, the cache store, media files and external services.](/blog/bookmymovie-case-study-cinema-ticketing-platform/architecture.webp)

The habit that saved me most often was keeping each set of rules in exactly one place.

Cancelling a booking is a good example. When a booking is cancelled, a lot has to happen together: the seats go back on sale, the show's sold count drops, a used coupon is handed back, gift card money is refilled, loyalty points are reversed, the payment record is updated, and anyone on the waitlist hears that seats are free. A customer cancelling from their phone and a manager pressing "refund" both go through the same piece of code (`BookingLifecycle::unwind()`), so the two can never quietly disagree.

The same idea runs through the rest. One class decides every page's title and its preview card when it's shared. Another finds each film's poster and trailer by name, so adding a new trailer means dropping three files into a folder. Anything slow, like sending a receipt, goes into a queue and happens a moment later in the background. And a scheduler wakes up every minute to release seat holds that have run out of time.

## The database that says no

![The booking core of the data model: cities, theaters, screens and seats; movies and shows with row prices; users, carts and cart items; bookings, booking seats, payments and splits; reviews and waitlists.](/blog/bookmymovie-case-study-cinema-ticketing-platform/erd.webp)

There are 56 tables and 9 reporting views underneath, but the shape is easy to follow. A city has cinemas, a cinema has screens, and a screen has seats. A show is one film on one screen at one time. Your cart holds seats for ten minutes, and paying turns the cart into a booking with one ticket per seat.

![The cinemas page: six cinemas in four cities, 13 screens and 1,224 seats, all read from the same city, theater, screen and seat tables.](/blog/bookmymovie-case-study-cinema-ticketing-platform/cinemas.webp)

What I like most about this database is what it refuses to do, no matter what the code above it asks:

- Two films can't be booked onto the same screen at the same time.
- A seat can sit in only one person's cart.
- A seat can have only one live ticket per show. Each ticket carries a small marker that reads 1 while the ticket is valid and is cleared when it's cancelled. The database allows any number of cleared markers but only one live one, so a cancelled seat can be sold again, yet nothing, not even a bug of mine, can create a second live ticket for it. I wrote a test that skips all my code and tries to sneak a duplicate straight into the database. The database throws it out.
- Every checkout carries a one-time key, so a double-tapped "Confirm" returns the booking that was already made instead of charging you twice.
- One review per person per film, and one waitlist place per person per show.

The reporting views do the heavy counting in advance, so the seat map and every chart in the back office need only one question to the database each.

## Booking a seat, step by step

Every booking starts from a film's showtimes: one row per cinema, the cheapest seat on each show, the matinée discount shown in red, and a warning once a show is more than 60% sold.

![A film's showtimes: a date strip, city filters and each cinema's shows with the lowest seat price.](/blog/bookmymovie-case-study-cinema-ticketing-platform/film-showtimes.webp)

![The seat map: row-tier prices, best-seat suggestions, a seat map / 3D hall switch, and the selection summary with the total.](/blog/bookmymovie-case-study-cinema-ticketing-platform/seats.webp)

1. **You pick seats.** Each row has its own price. Front rows are cheapest, the "Prime Centre" rows have the best view, and the recliners are at the back. Weekday shows before 5 PM are 15% cheaper, and child tickets are about 25% cheaper (but not on recliners). If you don't want to think about it, "best 2 together" finds side-by-side seats near the middle, about two-thirds of the way back.
2. **The seats are held for you.** For ten minutes they're yours, with a countdown at the top of the screen. Nobody else can take them while you find your card.
3. **You check out.** The server works out the price again from scratch: seats, coupon, gift card, points. It never trusts a number sent from your phone.
4. **You get a ticket.** It carries a QR code with a signature, so the person at the door can tell a real ticket from an edited screenshot.

Offers have their own page, with the minimum spend, the maximum discount and the end date printed on each one, so the price at checkout never comes as a surprise.

![The offers page: live coupon codes with their minimum spend, maximum discount and end date.](/blog/bookmymovie-case-study-cinema-ticketing-platform/offers.webp)

## The half-second that matters

Here's the moment the sticky note was really about. Two people have seat G7 open on their phones. Both tap it. Both press Confirm within the same half-second.

If the server simply asks "is G7 free?" and then "book G7", both requests can hear "yes" before either one has booked anything, and the seat is sold twice. So the server doesn't ask and then act. It locks the seat first, like putting a hand on the last cinema chair, and only then checks and books. The second request has to wait until the first is finished, and by then the answer is "taken".

![Hold, sell, and the only ways out: available, held, confirmed, then completed, no-show or cancelled. Cancelling puts the seat back on sale and tells the waitlist.](/blog/bookmymovie-case-study-cinema-ticketing-platform/lifecycle.webp)

The order of the locks matters just as much as the locks. If one request grabs the customer and then the seat, while another grabs the seat and then the customer, each can end up holding one and waiting forever for the other. So every part of the system takes its locks in the same order: customer, then show, then seats, then cart. The rare tangle that still happens is simply retried, up to three times.

I didn't want to just believe this works, so I staged a small opening night. I packed the whole site into Docker the way it would run on a real server, then sent in fifteen pretend visitors, each from their own address. Four of them browsed. Three were cinema staff (an owner, a manager and someone at the box office) clicking through the back office. The other eight were buyers, and all eight pressed "hold" on the same seat at the same instant.

One of them got it. The other seven were told straight away that the seat had just been taken, which is exactly what should happen. Each buyer then double-tapped Confirm on their own seats, and each tap landed on one booking, never two. Across about 1,900 requests there wasn't a single server error, and a typical page came back in about a tenth of a second.

I wrote a separate, more technical piece about this part, including the bugs: [One seat, two buyers: race conditions, IDOR and keys in BookMyMovie](/blogs/one-seat-two-buyers-race-conditions-idor-and-keys-in-bookmymovie).

## Behind the counter

Most of the daily work in a cinema happens in the back office, so I rebuilt it around one question: what does this person need to do right now? The menu is grouped by job (Today, Catalogue, People, Reports, Settings), each item has a one-line hint, and each page opens with a short "How this page works" guide you can fold away once you know it.

![The admin overview: the grouped sidebar, the page guide and the 14-day revenue with a 3D skyline of daily takings.](/blog/bookmymovie-case-study-cinema-ticketing-platform/admin-overview.webp)

The overview answers "is tonight going well?" in one look. Each screen gets its own lane, each show's bar fills up as seats sell, and a red line marks the time right now. A "Needs you" list points straight at unpaid bookings and unread messages.

![Tonight on every screen: one lane per screen, each show filled by the share of seats sold, and a live now-line.](/blog/bookmymovie-case-study-cinema-ticketing-platform/admin-timeline.webp)

Editing a film is a numbered form (basics, price and rating, artwork, home carousel, search engines) with a single save bar that tells you whether anything is unsaved. Every box shows an example of what to type, so nobody has to guess what a "slug" is.

![The film editor: a searchable film list on the left and numbered sections with example placeholders on the right.](/blog/bookmymovie-case-study-cinema-ticketing-platform/admin-movie-editor.webp)

A refund is one button. Behind it sits the same cancellation code a customer uses, so the seats, the coupon, the gift card balance and the points all come back, and the customer gets an e-mail.

![Bookings and refunds: recent bookings with their payment and booking status and a one-step cancel.](/blog/bookmymovie-case-study-cinema-ticketing-platform/admin-bookings.webp)

Three roles decide who sees what. The owner can do everything. A manager runs films, prices, coupons, members and refunds. Box-office staff see the dashboard and bookings and check tickets at the door. Everyone signs in with a code from an authenticator app on their phone, and banning a troublemaker also blocks every internet address and browser they've used.

![Staff and roles: owner, manager and box office described in plain words, and the staff table with each person's role and two-step status.](/blog/bookmymovie-case-study-cinema-ticketing-platform/admin-staff.webp)

![A member's page: profile, spend and points beside a ban form that can also block their browsers and IP addresses, with a note that shared IPs can catch innocent people.](/blog/bookmymovie-case-study-cinema-ticketing-platform/admin-member.webp)

Taking these screenshots turned up two layout bugs I had walked past many times: form labels that drifted away from their boxes whenever a row grew taller, and a staff table that cut off its last column. Both are fixed. The first took one line of CSS, and it tidied every form on the site at once.

## How it looks and feels

The brief I gave myself was cinema signage: near-black, warm paper white, and one electric yellow-green for the thing you should press. Titles use tall, narrow capitals, the way a marquee spells out a film's name, and anything that is data (times, prices, booking numbers) is set in a typewriter-style font so it's easy to read at a glance.

A few parts I'm proud of:

- **Trailers everywhere.** Short silent trailers play behind the home page and the film pages. To spare people's data, they only play on a fast connection with Data Saver off, and a still image shows first.
- **Sit before you buy.** A 3D model of the hall lets you sit in any seat, watch the trailer on the big screen as the lights dim, and see a view score based on how far you are, how much of your view the screen fills, how far off-centre you are and how much you'd tilt your neck.
- **Two themes.** Dark and light, built from the same set of colours. Both pass the contrast checks for readability. They didn't at first; more on that below.
- **Urdu.** One switch turns the whole site into Urdu, reading right to left. Film titles, prices and booking numbers stay in English letters, so they match the ticket in your hand.
- **Usher.** A small helper that answers preset questions like "what's the cheapest seat tonight?" or "where can I watch in IMAX?" using live listings. If you're signed in, it knows your next show, your points and which films on your watchlist are on sale. There's no typing box on purpose, so nothing you write is ever sent anywhere.

![Usher answering "What's good this week?" from live listings, with preset questions underneath and no free-text box.](/blog/bookmymovie-case-study-cinema-ticketing-platform/usher.webp)

![The home page in the light theme: the same tokens redefined as warm paper and ink, with the volt accent kept for the one action that matters.](/blog/bookmymovie-case-study-cinema-ticketing-platform/home-light.webp)

On a phone, everything folds into a single column, and no page ever scrolls sideways, even on a screen 320 pixels wide. I don't check that by eye. A script opens every page at five different widths and complains if anything spills over.

![The home page on a phone: the trailer carousel with full-width Book tickets and Watch trailer buttons.](/blog/bookmymovie-case-study-cinema-ticketing-platform/m-home.webp)
![BookMyMovie's seat map on a phone.](/blog/bookmymovie-case-study-cinema-ticketing-platform/m-seats.webp)

## Being findable, by people and by AI

A site that builds its pages in the browser can look empty to a search engine, so the server does extra work for them:

- Every page's title, description, link preview image and indexing rule is written into the page before any JavaScript runs. Each film gets its own square preview image, so a shared link looks like a poster.
- Hidden structured data tells search engines what the page is about: the cinema chain, the breadcrumb trail, and for each film its cast, rating and every upcoming screening with prices.
- A sitemap lists every page, and two plain-text files (`llms.txt` and `llms-full.txt`) describe the site and its whole catalogue for AI assistants. One command rebuilds them from the database.
- Pages that shouldn't appear in search (search results, password resets, the back office, error pages) are deliberately kept out.

Lighthouse, Google's quality checker, scores 100 for accessibility, best practices, SEO and agentic browsing on every public page.

## Keeping it safe

| What could go wrong | What stops it |
|---|---|
| Stolen or guessed passwords | Passwords stored with Argon2id, which is slow to crack on purpose; optional e-mailed codes for customers; phone-app codes for staff |
| Seeing someone else's booking | Every lookup is tied to the person signed in, so a guessed booking number just shows "not found" |
| Staff reaching too far | Each admin page is mapped to a role, and anything not on the map needs the owner |
| Bots | A hidden trap field, a minimum time to fill a form, an image code drawn by the server, Google's invisible reCAPTCHA with a checkbox only when it's unsure, rate limits, and bans that cover the account, its addresses and its browsers |
| Malicious scripts in the page | A strict content security policy with a fresh secret for every page, and a CSRF token on every form |
| Fiddled payments | Amounts are worked out again on the server, and payment callbacks are checked by signature and amount before anything changes |
| Fake tickets | Every QR code is signed, and the signature is checked at the door |
| Photo uploads | Every image is re-encoded, which also strips the hidden location data from phone photos |
| "Who changed this?" | Every change by staff and customers is logged with the before and after values |

## Making it fast

- **Images** come in two sizes so a phone never downloads a desktop poster. Converting the original artwork from PNG to WebP took it from about 85 MB down to 5.4 MB.
- **Trailers** use the smaller WebM format first, with MP4 as a backup. The latest one went from 3.4 MB to 1.1 MB.
- **Caching** happens in five layers, from your browser to the database, so popular pages like the home page are served from memory. When a cached page goes stale, only one request rebuilds it while everyone else is served the slightly older copy, so a busy moment can't stampede the database.

## The bugs that got through

This is the part I'd most want to read in someone else's case study.

**The seat map broke in the real build.** While I was developing, everything worked. In the production build, the most important page on the site, the seat map, crashed for every visitor. The build tool had quietly folded that page into another file, so the server couldn't find it. I only noticed while taking screenshots for this article. The fix was one line. The lesson was a new test that checks every page exists in the finished build.

**Offline tickets didn't work at all.** The feature I talked about most was silently broken. The background helper that saves tickets for offline use tried to save four files at once, one file path was wrong, and when one fails, the whole batch fails. The only clue was a vague error in the browser console. Now each file is saved separately, and a missing one is skipped.

**Two captchas at once.** When I switched on Google's real captcha, forms showed the "I'm not a robot" checkbox and the invisible version together, and asked visitors to pass both. I redesigned it so the invisible check runs on its own, and the checkbox appears only when Google isn't sure.

**Buttons you couldn't read.** In the review section, the selected sort button picked up a light background from one rule and bright text from another. It was yellow-green on white in the dark theme and olive on black in the light theme. Both were unreadable. Some of the quietest grey text was also too faint. I fixed the shared colours rather than patching each screen.

![Film reviews in the dark theme after the fix: the selected Newest chip is dark on light, the others light on dark.](/blog/bookmymovie-case-study-cinema-ticketing-platform/film-reviews.webp)
![The same reviews in the light theme: rating breakdown, sort chips and verified-booking badges.](/blog/bookmymovie-case-study-cinema-ticketing-platform/film-reviews-light.webp)

**Phones scrolled sideways.** On the help page, a row of category buttons stretched the layout to 968 pixels on a 375-pixel phone. One shared layout piece now refuses to grow wider than the screen, and the script I mentioned earlier checks every page at five widths.

**Popcorn broke checkout.** This one only turned up during the pretend opening night, weeks after I called the project finished. The database had a rule saying a booking's total must equal the ticket price minus any discount. That was true when I wrote it. Then I added snacks, gift cards and loyalty points, all of which change the total, and never updated the rule. So anyone who added popcorn, or paid with a gift card or points, was turned away, and the message blamed someone else for taking their seat. None of my tests had paid that way. The rule now accounts for everything, and there are tests that buy popcorn and pay with a gift card and points together.

**Old links went nowhere.** I moved the seat map to a new address, and every bookmarked or shared link broke. They now redirect to the new address permanently.

## Honest weak spots

- **Search engines that don't run JavaScript see an empty page body.** The title, structured data and sitemap cover most of what they need, but rendering the page on the server would close the gap, at the cost of running an extra process.
- **It's heavy on cheap phones.** The 3D scenes, a trailer and two variable fonts are a lot for a mid-range phone on a slow network. The 3D parts load late and are skipped on slow connections, but the first visit is still heavy.
- **Most admin forms go through one door.** Many of them post to a single address with an action name. The role check covers every action and refuses anything unknown, but one route per action would be easier to audit.
- **A hand-set star rating.** Admins can show a manual rating for a film's launch week. It's labelled internally, but it's the kind of feature that becomes a dark pattern in the wrong hands, and I'd remove it for a real cinema.
- **Short ticket signatures.** To keep QR codes quick to scan, a ticket carries only part of its signature. Guessing one would mean hammering a page only staff can reach, but it's still a trade-off.
- **No real money has moved.** The JazzCash and Easypaisa payment flows, signatures and refunds are built and tested, but only against the sandbox.
- **Tests stop at the server.** 46 automated tests cover booking, payments, bans, roles, waitlists and group bookings on a real MySQL database. There are no end-to-end tests in a real browser yet.

## Where it stands now

BookMyMovie is finished as a portfolio project. It isn't running anywhere public; the code is on [GitHub](https://github.com/ahmershahdev/bookmymovie) under the MIT licence, with a [wiki](https://github.com/ahmershahdev/bookmymovie/wiki) that walks through setup, the booking rules and the security design. If I picked it up again for a real cinema, this is the order I'd go in:

1. Render pages on the server and measure the phone speed again.
2. Give every admin action its own route.
3. Add browser tests where two real browsers fight over one seat.
4. Put the films and trailers behind a CDN.
5. Launch with one partner cinema and real payments.

## What it taught me

The interesting part of a booking system isn't the booking form. It's the half-second when two people press Confirm on the same seat, the payment message that arrives twice, the manager refunding a booking at the exact moment the customer is cancelling it, and the build tool that quietly moves your page into the wrong file.

Most of what I'm proud of in BookMyMovie is invisible when it works. If you book a seat and walk in without thinking about any of this, it did its job.


---

*Originally published at https://ahmershah.dev/blogs/bookmymovie-case-study-cinema-ticketing-platform — © Syed Ahmer Shah*
