Syed Ahmer ShahSyed Ahmer ShahSyed Ahmer ShahSyed Ahmer ShahSyed Ahmer Shah — home
● MenuAHMERAHMER
20+Certificates
PHPMERNMySQLSEO

Explore

01About→02Education→03Certificates→04Skills→05Projects→06Services→07Testimonials→08Contact→

Pages

01Blog→02Saved articles→03Privacy Policy→04Terms of Service→
SoundHire me →

Available for new work · --:--:-- PKT

Have an idea?

support@ahmershah.dev
Book a call WhatsApp +92 370 4831994

On this site

  • 01About
  • 02Education
  • 03Certificates
  • 04Skills
  • 05Projects
  • 06Services
  • 07Testimonials
  • 08Contact

Pages

  • Blog
  • Saved articles
  • RSS
  • Privacy Policy
  • Terms of Service
  • llms.txt

Elsewhere · official profiles

26 links

Professional

Work history, code and credentials.

  • in/syedahmershah
  • company/syedahmershah
  • @ahmershahdev
  • syed-ahmer-shah
  • syed-ahmer-shah
  • u/syedahmershah
  • @syedahmershah
  • AWS@syedahmershah

Coding & credentials

Problem solving, verified badges.

  • u/syedahmershah
  • syedahmershah
  • users/syedahmershah
  • learner/syedahmershah

Google & business

The official listings.

  • Google Business Profileg.page
  • Syed Ahmer Shah
  • syedahmershah
  • Beaconssyedahmershah
  • syedahmershah

Social (@ahmershahdev)

Build logs, clips, threads.

  • @ahmershahdev
  • @ahmershahdev
  • @ahmershahdev
  • ahmershahdev
  • @ahmershahdev
  • @bluesky.ahmershah.dev
  • ahmershahdev

Design

Interfaces and visuals.

  • syedahmershah
  • syedahmershah

Direct

  • support@ahmershah.dev

    Support & projects

  • syedahmershahofficial@gmail.com

    Personal

  • +92 370 4831994

    Phone · WhatsApp

  • Hyderabad, Sindh, Pakistan

    Remote-first · Asia/Karachi (UTC+5)

Résumé ↗
SYED AHMER SHAH

© 2026 Syed Ahmer Shah. All rights reserved.

PrivacyTermsRSS
Syed Ahmer ShahSyed Ahmer ShahSyed Ahmer ShahSyed Ahmer ShahSyed Ahmer Shah — home
● MenuAHMERAHMER
20+Certificates
PHPMERNMySQLSEO

Explore

01About→02Education→03Certificates→04Skills→05Projects→06Services→07Testimonials→08Contact→

Pages

01Blog→02Saved articles→03Privacy Policy→04Terms of Service→
SoundHire me →

Available for new work · --:--:-- PKT

Have an idea?

support@ahmershah.dev
Book a call WhatsApp +92 370 4831994

On this site

  • 01About
  • 02Education
  • 03Certificates
  • 04Skills
  • 05Projects
  • 06Services
  • 07Testimonials
  • 08Contact

Pages

  • Blog
  • Saved articles
  • RSS
  • Privacy Policy
  • Terms of Service
  • llms.txt

Elsewhere · official profiles

26 links

Professional

Work history, code and credentials.

  • in/syedahmershah
  • company/syedahmershah
  • @ahmershahdev
  • syed-ahmer-shah
  • syed-ahmer-shah
  • u/syedahmershah
  • @syedahmershah
  • AWS@syedahmershah

Coding & credentials

Problem solving, verified badges.

  • u/syedahmershah
  • syedahmershah
  • users/syedahmershah
  • learner/syedahmershah

Google & business

The official listings.

  • Google Business Profileg.page
  • Syed Ahmer Shah
  • syedahmershah
  • Beaconssyedahmershah
  • syedahmershah

Social (@ahmershahdev)

Build logs, clips, threads.

  • @ahmershahdev
  • @ahmershahdev
  • @ahmershahdev
  • ahmershahdev
  • @ahmershahdev
  • @bluesky.ahmershah.dev
  • ahmershahdev

Design

Interfaces and visuals.

  • syedahmershah
  • syedahmershah

Direct

  • support@ahmershah.dev

    Support & projects

  • syedahmershahofficial@gmail.com

    Personal

  • +92 370 4831994

    Phone · WhatsApp

  • Hyderabad, Sindh, Pakistan

    Remote-first · Asia/Karachi (UTC+5)

Résumé ↗
SYED AHMER SHAH

© 2026 Syed Ahmer Shah. All rights reserved.

PrivacyTermsRSS
Syed Ahmer ShahSyed Ahmer ShahSyed Ahmer ShahSyed Ahmer ShahSyed Ahmer Shah — home
● MenuAHMERAHMER
20+Certificates
PHPMERNMySQLSEO

Explore

01About→02Education→03Certificates→04Skills→05Projects→06Services→07Testimonials→08Contact→

Pages

01Blog→02Saved articles→03Privacy Policy→04Terms of Service→
SoundHire me →
← Engineering Logs/The Engineering Logs · part 9 of 10

BookMyMovie: Building a Cinema Ticketing Platform

A case study on selling cinema seats online without ever selling the same seat twice, and on everything around that one rule: trailers, QR tickets, Urdu, the back office and the bugs that got through

Written by

Syed Ahmer Shah

Software Engineer

Sep 29, 202617 min read3,779 words—reading now▼
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
Read this articlevoice
Markdown

Buying a cinema ticket in Pakistan still often means calling the cinema, queueing at the counter, or using a site that shows you a seat map, lets you pick a seat, and then tells you at the last step that somebody else already has it. Prices are rarely clear before you commit, premium screens (IMAX, Dolby, recliners) are hard to compare across cinemas, and the ticket is usually a screenshot you hope the usher will accept.

BookMyMovie is my answer to that. It started as my Aptech DISM project in July 2026 and ended up as a full platform that I designed and built alone: the research, the UI, the database, the backend, payments, security, SEO, the admin back office, tests and documentation. You browse films, watch trailers, pick seats on a live map (or from inside a 3D model of the hall), add snacks, book, and get a signed QR e-ticket that still opens with no signal.

This is how it's put together, what went wrong, and what I'd change.


#The one rule

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

A shop can apologise for a late parcel. Two people holding tickets for G7 at the 9 PM show is a scene in the lobby. Almost every design decision below comes back to that sentence.

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.
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.

#Who uses it

  • Moviegoers browse films and cinemas, watch trailers, compare up to four films, pick seats, mix adult and child tickets, add snacks, apply a coupon, a gift card or loyalty points, and pay online or at the counter. They get a QR e-ticket (with Apple and Google Wallet passes when configured), a watchlist, a waitlist for sold-out shows, a way to split the bill with friends, and "verified booking" reviews.
  • Box-office staff scan tickets at the door, refund bookings, issue gift cards and look up members.
  • Managers and the owner run films, showtimes, row prices, coupons, snack stock, reviews, bans, site settings and SEO, and read revenue and occupancy reports. Three roles (owner, manager, box office) are enforced on every admin route, with authenticator-app two-step sign-in.
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.
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.

#Choosing the stack

I wanted an interface that feels like an app, with no full page reloads, and a backend where routing, authentication and validation stay on the server where I can reason about them.

Laravel 12 and React 19, joined by Inertia 2, gave me both. Every page is a React component, but there's no public JSON API to secure. A controller validates the request, checks who is asking, and hands props to the page. The layout never unmounts, so the navbar, the seat-hold countdown and the assistant survive navigation.

Layer Choice Why
Backend Laravel 12, PHP 8.2 Routing, Eloquent, queues, rate limiting, mail
Bridge Inertia 2 + Ziggy SPA navigation with server-side routing and auth
Frontend React 19, strict TypeScript, Tailwind CSS 4 Components, one set of design tokens for two themes
Motion and 3D Motion, Lenis, Three.js via @react-three/fiber Page motion, smooth scroll, the poster ring and the 3D hall
Database MySQL 8 / MariaDB Foreign keys, unique guards, reporting views, row locks
Payments JazzCash, Easypaisa, card, pay at counter What people in Pakistan actually use
Mail and jobs Resend, database queue E-mail never blocks a page

#Architecture

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.
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.

The rule I kept coming back to was thin controllers, and one class for every set of rules that must hold together. Cancelling a booking is a good example. It has to put the seats back on sale, lower the show's sold count, return the coupon use, refill the gift card, reverse the loyalty points, update the payment and tell the waitlist. A customer cancelling from their phone and an admin issuing a refund go through the same BookingLifecycle::unwind(), so the two can never drift apart.

Other decisions that paid off:

  • One source of truth per concern. App\Support\Seo builds titles, Open Graph and JSON-LD for every page. App\Support\MovieMedia links artwork and trailers by the film's slug, so adding a trailer is "drop three files in a folder and run a migration".
  • Anything slow goes on the queue. Receipts, waitlist alerts and web push are jobs, retried on failure.
  • The scheduler does the boring, important work: expiring 10-minute seat holds every minute, sweeping waitlists and draining the queue where no long-running worker exists.

#The data model

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.
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.

The schema is 56 tables and 9 reporting views. The shape is simple once you see it: a city has theaters, a theater has screens, a screen has seats. A show is one film on one screen at one time. A customer's cart holds seats for ten minutes; checkout turns it into a booking with one booking seat per ticket.

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.
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.

What makes it trustworthy is what the database itself refuses:

  • shows (screen_id, show_date, show_time) is unique: two films can't be scheduled on the same screen at once.
  • cart_items (seat_id, show_id) is unique: a seat can sit in only one cart.
  • booking_seats (show_id, seat_id, seat_lock) is unique, and seat_lock is 1 while a ticket is live and NULL once it's cancelled. MySQL lets a unique index hold any number of NULLs, so a cancelled seat can be sold again, but no bug, script or admin can ever insert a second live ticket for the same seat. I have a test that bypasses every piece of PHP and inserts the row by hand; the database throws.
  • bookings.idempotency_key is unique: a double-tapped "Confirm" returns the booking it already made.
  • reviews (user_id, movie_id) and show_waitlists (show_id, user_id) are unique: one review per film, one waitlist place per show.

The reporting views (v_seat_availability, v_show_details, v_daily_revenue and six more) keep the seat map and every dashboard chart down to a single query each.

#Booking a seat

Every booking starts from a film's showtimes: one row per cinema, the lowest seat price on each show, the matinée discount 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.
A film's showtimes: a date strip, city filters and each cinema's shows with the lowest seat price.
The seat map: row-tier prices, best-seat suggestions, a seat map / 3D hall switch, and the selection summary with the total.
The seat map: row-tier prices, best-seat suggestions, a seat map / 3D hall switch, and the selection summary with the total.
  1. Pick seats. Each row has its own price (front rows cheapest, "Prime Centre" best, recliners at the back), weekday shows before 5 PM carry a 15% matinée discount, and child tickets are about 25% cheaper (not on recliners). "Best 2 together" finds adjacent seats near the centre, about two-thirds of the way back.
  2. Hold. Adding seats to the cart runs one transaction: lock the customer, then the show, then the seats, then the cart, check availability, insert the holds. The seats are yours for ten minutes, with a countdown in the navbar.
  3. Check out. The same lock order again. Prices, coupon limits, gift card balance and points are all re-checked inside the transaction, never trusted from the browser.
  4. Ticket. The booking gets a QR code carrying an HMAC signature, so the box office can tell a real ticket from an edited one.

Coupons have their own page, with the minimum spend, the cap and the end date printed on each one, so the discount at checkout is never a surprise.

The offers page: live coupon codes with their minimum spend, maximum discount and end date.
The offers page: live coupon codes with their minimum spend, maximum discount and end date.
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.
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.

The fixed lock order (user → show → seats → cart) matters as much as the locks. Two requests that take the same locks in a different order can each hold one lock and wait forever for the other. Taking them in one order everywhere means one request simply waits for the other to finish. Deadlocks that still happen are retried up to three times.

I wrote a separate article about the concurrency and security side, including the bugs: One seat, two buyers: race conditions, IDOR and keys in BookMyMovie.

#The back office

The admin side is where most of the day-to-day work happens, so I rebuilt it around one question: "what does this person need to do right now?" The sidebar is grouped by job (Today, Catalogue, People, Reports, Settings), every item has a one-line hint, and every page opens with a short "How this page works" guide that can be tucked 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.
The admin overview: the grouped sidebar, the page guide and the 14-day revenue with a 3D skyline of daily takings.

The overview answers "is tonight going well?" at a glance. The timeline gives each screen its own lane and fills each show's bar by the share of seats sold, with a red line for the time right now. A "Needs you" list links straight to the unpaid bookings and unread messages that want a person.

Tonight on every screen: one lane per screen, each show filled by the share of seats sold, and a live now-line.
Tonight on every screen: one lane per screen, each show filled by the share of seats sold, and a live now-line.

Editing a film is a numbered form (basics, price and rating, artwork, home carousel, search engines) with one save bar that says whether anything is unsaved. Every input shows an example value, so nobody has to guess what "slug" means.

The film editor: a searchable film list on the left and numbered sections with example placeholders on the right.
The film editor: a searchable film list on the left and numbered sections with example placeholders on the right.

Refunds are one button. Behind it, the same BookingLifecycle::unwind() a customer's cancellation uses puts the seats back on sale and returns the coupon use, gift card balance and loyalty points, and the customer is e-mailed.

Bookings and refunds: recent bookings with their payment and booking status and a one-step cancel.
Bookings and refunds: recent bookings with their payment and booking status and a one-step cancel.

Three roles decide who sees what: the owner can do everything, a manager runs films, prices, coupons, members and refunds, and box-office staff see the dashboard and bookings and check tickets at the door. Each person's two-step status is visible at a glance, and banning a member also blocks every IP and browser they 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.
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.
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.
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.

Taking these screenshots found two layout bugs I had missed: a shared form-field style that stretched labels away from their inputs whenever a row was taller than the field, and a staff table that cut off its last column. Both are fixed; the first was one line of CSS (align-content: start) that tidied every form on the site.

#Design

The design brief I set myself was "cinema signage": near-black ink, warm paper, and one electric volt accent, with condensed, variable-width Archivo capitals the way a marquee spells a title. Geist Mono carries anything that is data (times, prices, booking numbers). Edges are hard; the only curves are the screen on the seat map.

A few pieces I'm proud of:

  • Trailers everywhere. Short silent trailers play behind the home carousel and film pages, with a WebP still first. They only autoplay when motion is allowed, Data Saver is off and the connection is faster than 3G.
  • See it from your seat. A Three.js model of the hall: sit in any seat, watch the trailer on the screen as the lights dim, and get a view score (distance, how much of your view the screen fills, off-centre angle and neck tilt).
  • Two themes, one set of tokens. Every colour is a CSS variable, and dark and light themes redefine them. Both pass WCAG AA contrast. They didn't at first (more on that below).
  • Urdu. A full English/Urdu switch with a right-to-left layout and Noto Naskh Arabic. Film titles, prices and booking numbers stay in Latin script so they match the ticket in your hand.
  • Usher. A small assistant that answers preset questions ("cheapest seat tonight?", "where can I watch in IMAX?") from live listings. Signed-in customers get personal answers: their next show, their points, which watchlist films are on sale, and a pick based on the genres they book. It has no free-text box on purpose: nothing a visitor types is ever sent anywhere.
Usher answering "What's good this week?" from live listings, with preset questions underneath and no free-text box.
Usher answering "What's good this week?" from live listings, with preset questions underneath and no free-text box.
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.
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.

On a phone the whole thing collapses to one column without a single sideways scroll, from 320 px up. I check that with a script rather than by eye.

The home page on a phone: the trailer carousel with full-width Book tickets and Watch trailer buttons.
The home page on a phone: the trailer carousel with full-width Book tickets and Watch trailer buttons.
BookMyMovie's seat map on a phone.
BookMyMovie's seat map on a phone.

#SEO, and search for AI

Inertia renders the page body in the browser, so the server has to do the rest of the work for crawlers:

  • Server-rendered head. Every page's title, description, canonical URL, Open Graph image (generated per film, 1200×1200) and robots rule come from the controller and are written into the HTML before any JavaScript runs.
  • JSON-LD. Organization, WebSite with a search action, BreadcrumbList, and for each film a Movie with its cast, rating and every upcoming ScreeningEvent with offers.
  • A generated sitemap (64 URLs with film images) and llms.txt / llms-full.txt, a plain-text description of the site and its whole catalogue for AI assistants, regenerated from the database with php artisan seo:generate.
  • Deliberate noindex. Search results, password reset and the admin area are kept out of search engines. Lighthouse marks those pages down for it, and I accept that.

Lighthouse's accessibility, best practices, SEO and agentic-browsing categories score 100 on every public page (the noindex pages lose SEO points by design, and admin login loses a few best-practice points to Google reCAPTCHA's own iframe).

#Security

Area What protects it
Passwords and sign-in Argon2id; optional e-mailed 6-digit codes for customers; authenticator-app TOTP with recovery codes for staff; sessions regenerated on sign-in
Access control Every customer query is scoped to the signed-in user, so a guessed booking number is a 404; admin roles are mapped per route and anything not in the map needs the highest role (it fails closed)
Bots Honeypot, minimum fill time, a server-drawn single-use image code, invisible reCAPTCHA v3 with the v2 checkbox only as a step-up, rate limits, and bans that cover the account, every IP and every browser it used
Browser Nonce-based Content-Security-Policy, HSTS, X-Frame-Options: DENY, CSRF on every form
Payments Amounts recomputed on the server; JazzCash callbacks verified by signature and amount before anything changes; payment and booking rows locked while settling
Tickets HMAC-signed QR payloads, checked with a constant-time comparison at the door
Uploads Re-encoded to WebP, which also strips EXIF and GPS data
Audit Every data-changing action by staff and customers is logged with before and after values

#Performance and caching

  • Images ship in two sizes (card-sm.webp 640 px and card.webp 1200 px) with srcset, explicit dimensions and lazy loading. The server tells the browser to preload the one image that is the largest paint, plus the two text fonts. Converting the original artwork from PNG to WebP took it from about 85 MB to 5.4 MB.
  • Trailers are VP9 WebM first with an H.264 MP4 fallback. The latest one went from 3.4 MB to 1.1 MB.
  • Five cache layers: HTTP (hashed assets cached for a year, film art for a month), the service worker (offline tickets, stale-while-revalidate film art), the application cache (home rails and menus served stale while one request rebuilds them under a lock, so an expiry never stampedes the database), a per-request memo, and the database views.

#Where it broke

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

The seat map returned a 500 in production builds. The seat map page and the lazy-loaded 3D hall both imported a small geometry file. Rollup quietly merged that file into the page's own chunk. The page then had no entry of its own in Vite's manifest, and Laravel answered every visit to the most important page on the site with "Unable to locate file in Vite manifest". Development mode hid it completely. I found it while taking screenshots for this article. The fix was one line (give the shared file its own chunk). The lesson was a test that checks every page is in the manifest, which now runs with the others.

The service worker never installed. It pre-cached four files with cache.addAll(), one path was wrong, and addAll rejects the whole batch if any one file fails. So offline tickets, the feature I talk about most, silently didn't work, and the only symptom was an "Uncaught (in promise)" in the console. Now each file is cached on its own and a missing one is skipped.

Two captchas at once. When the real reCAPTCHA keys went in, forms showed the v2 checkbox and v3's floating badge, and the server required both. I redesigned it: v3 runs invisibly on every submit, and the checkbox appears only as a step-up when v3's score is too low or it fails to load.

Unreadable buttons. The selected "Newest / Most helpful / Highest / Lowest" chip got a light background from one rule and accent-coloured text from another. In the dark theme that was volt on off-white; in the light theme it was olive on near-black. Both were unreadable. The quietest grey text was also below 4.5:1 contrast. I fixed the tokens, not the individual screens.

Film reviews in the dark theme after the fix: the selected Newest chip is dark on light, the others light on dark.
Film reviews in the dark theme after the fix: the selected Newest chip is dark on light, the others light on dark.
The same reviews in the light theme: rating breakdown, sort chips and verified-booking badges.
The same reviews in the light theme: rating breakdown, sort chips and verified-booking badges.

Phones scrolled sideways. A CSS grid with no column template sizes its one column to its widest content, so on the FAQ a row of category buttons made the page 968 px wide on a 375 px phone. The shared grid component now defaults to minmax(0, 1fr), and a script loads every page at 320, 375, 414, 768 and 1024 px and fails if anything scrolls sideways.

Old links 404'd. Seat maps moved from /movies/{film}/{show} to /movies/{film}/book/{show}, and bookmarked or shared links broke. They now redirect permanently.

#Downsides I'd be honest about in a review

  • No server-side rendering of the page body. The head, JSON-LD and sitemap cover search engines well, but a crawler that doesn't run JavaScript sees an empty body. Inertia SSR would fix that at the cost of a Node process on the server.
  • Mobile performance is the weak score. Lighthouse performance on a throttled phone is low on the film pages: Three.js, a trailer and two variable fonts are a lot for a mid-range phone. The 3D scenes are lazy and skipped on slow connections, but the first load is still heavy.
  • One door for most admin writes. Many admin forms post to a single endpoint with an _action field. The role check maps each action to a permission and fails closed, but a single dispatcher is harder to audit than one route per action.
  • "Set the rating by hand." The admin can show a hand-set star rating for launch week. It's clearly labelled internally, but it's the kind of feature that becomes a dark pattern in the wrong hands. In a real deployment I'd remove it.
  • Short ticket signatures. The QR carries 64 bits of an HMAC-SHA256 (16 hex characters) to keep the code small enough to scan quickly. Guessing one means brute-forcing an endpoint only staff can reach, but it's still a trade-off, not a free lunch.
  • Payments are sandbox-only. The JazzCash and Easypaisa flows, signatures and refunds are built and tested, but no real money has moved through them.
  • Tests stop at the server. 38 PHPUnit feature tests cover booking, payments, bans, roles, waitlists and group bookings on a real MySQL database, and I use scripted Lighthouse and headless-Chrome checks for the front end. There are no committed end-to-end browser tests yet.

#Where it stands

BookMyMovie is finished as a portfolio project. It isn't hosted anywhere; the code lives on GitHub, archived, with everything needed to run it locally in one migrate --seed. If I picked it up again for a real cinema, this is the order I'd work in:

  1. Turn on Inertia SSR and measure the phone performance again.
  2. Replace the admin dispatcher with one route per action.
  3. Add Playwright end-to-end tests for "two people, one seat" in two real browsers.
  4. Move cache, sessions and the queue to Redis and put the media behind a CDN.
  5. Go live with one partner cinema and real payments.

#What it taught me

The interesting work in a booking system isn't the booking form. It's the half-second where two people press "Confirm" on the same seat, the payment callback that arrives twice, the admin who refunds a booking the customer is cancelling at the same moment, 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.

The code is on GitHub under the MIT licence (archived, read-only), with the full data model, locking and caching design and every screen in the README.

Find me across the web.

Same person, every platform

Work & code

  • ASPortfolioahmershah.dev
  • LinkedInin/syedahmershah
  • GitHub@ahmershahdev
  • AWSAWS Builder Center@syedahmershah
  • Crunchbasesyed-ahmer-shah
  • LinkedIn · Companycompany/syedahmershah

Writing

  • Dev.tosyedahmershah
  • Medium@syedahmershah
  • Hashnode@syedahmershah
  • Substack@syedahmershah
  • HackerNoonu/syedahmershah
  • CLCoderLegionuser/syedahmershah

Reviews & listings

  • Google Knowledge PanelSyed Ahmer Shah
  • Google Business Profileg.page
  • ClClutch
  • TpTrustpilot
  • DRDesignRush
  • TBTechBehemoths

Social

  • YouTube@ahmershahdev
  • Instagram@ahmershahdev
  • TikTok@ahmershahdev
  • Facebookahmershahdev
  • X@ahmershahdev
#bookmymovie#casestudy#laravel#react#mysql#systemdesign#softwarearchitecture#websecurity
ShareXLinkedInRedditWhatsApp

← Previous in series

The Last Mango Problem: Race Conditions in a Real Marketplace

Next in series →

One Seat, Two Buyers: Race Conditions, IDOR and Keys in a Cinema Booking System

On this page

  • The one rule
  • Who uses it
  • Choosing the stack
  • Architecture
  • The data model
  • Booking a seat
  • The back office
  • Design
  • SEO, and search for AI
  • Security
  • Performance and caching
  • Where it broke
  • Downsides I'd be honest about in a review
  • Where it stands
  • What it taught me

The Engineering Logs

  1. 01The Edge Latency Lie: Solving Global Consistency
  2. 02Top 10 AI Tools Every Frontend Developer Should Know (2026 Guide)
  3. 03Change One Number. Take Everything.
  4. 04How Databases Work: From Tables to Query Execution
  5. 05The New Era of Software Engineering
  6. 06The Art of Object-Oriented Programming
  7. 07GleanGrid: Building a Pre-Order Marketplace for Farmers Markets
  8. 08The Last Mango Problem: Race Conditions in a Real Marketplace
  9. 09BookMyMovie: Building a Cinema Ticketing Platform
  10. 10One Seat, Two Buyers: Race Conditions, IDOR and Keys in a Cinema Booking System

Up next

Keep reading.

All 48 articles→
  1. 01

    GleanGrid: Building a Pre-Order Marketplace for Farmers Markets

    How I designed and built GleanGrid on my own, a Laravel and React marketplace where Hyderabad's farmers publish weekly stock and customers reserve it, pay online or at the stall, and collect it: architecture, data model, payments, security and UX.

    Sep 27, 202615 minThe Engineering Logs

  2. 02

    One Seat, Two Buyers: Race Conditions, IDOR and Keys in a Cinema Booking System

    How BookMyMovie makes sure one cinema seat is never sold twice, a guessed booking number shows nothing, and a leaked or missing key can't quietly break tickets: row locks, lock order, a NULL-tolerant unique key, status compare-and-set, idempotency, IDOR scoping, HMAC tickets, captcha step-up, and the bugs that still got through.

    Sep 29, 202611 minThe Engineering Logs

  3. 03

    The Last Mango Problem: Race Conditions in a Real Marketplace

    What happens when fifty people try to buy the last item, use a single-use coupon or book a nearly full pickup window at the same instant, the MySQL snapshot detail that almost overbooked a stall, and the quieter races that hide in stock forms, restocks and payments.

    Sep 27, 202610 minThe Engineering Logs

Available for new work · --:--:-- PKT

Have an idea?

support@ahmershah.dev
Book a call WhatsApp +92 370 4831994

On this site

  • 01About
  • 02Education
  • 03Certificates
  • 04Skills
  • 05Projects
  • 06Services
  • 07Testimonials
  • 08Contact

Pages

  • Blog
  • Saved articles
  • RSS
  • Privacy Policy
  • Terms of Service
  • llms.txt

Elsewhere · official profiles

26 links

Professional

Work history, code and credentials.

  • in/syedahmershah
  • company/syedahmershah
  • @ahmershahdev
  • syed-ahmer-shah
  • syed-ahmer-shah
  • u/syedahmershah
  • @syedahmershah
  • AWS@syedahmershah

Coding & credentials

Problem solving, verified badges.

  • u/syedahmershah
  • syedahmershah
  • users/syedahmershah
  • learner/syedahmershah

Google & business

The official listings.

  • Google Business Profileg.page
  • Syed Ahmer Shah
  • syedahmershah
  • Beaconssyedahmershah
  • syedahmershah

Social (@ahmershahdev)

Build logs, clips, threads.

  • @ahmershahdev
  • @ahmershahdev
  • @ahmershahdev
  • ahmershahdev
  • @ahmershahdev
  • @bluesky.ahmershah.dev
  • ahmershahdev

Design

Interfaces and visuals.

  • syedahmershah
  • syedahmershah

Direct

  • support@ahmershah.dev

    Support & projects

  • syedahmershahofficial@gmail.com

    Personal

  • +92 370 4831994

    Phone · WhatsApp

  • Hyderabad, Sindh, Pakistan

    Remote-first · Asia/Karachi (UTC+5)

Résumé ↗
SYED AHMER SHAH

© 2026 Syed Ahmer Shah. All rights reserved.

PrivacyTermsRSS