LLD-008 · LOW LEVEL DESIGN
Movie Ticket Booking
Protect scarce seats with atomic holds, deadline checks and explicit late-payment repair.
Local Java 21 reference verified: 26 checks. Learner drafts are not executed. Author self-review; independent review pending.
Existing introductory lesson · Version 1
Understand the problem
Design seat reservation for several shows. Hold a group atomically for one owner until a deadline, confirm a trusted successful payment, and handle expiry, cancellation and duplicate callbacks without selling the same seat twice. Explain why payment success cannot overrule ownership that has already expired.
Before you start
- Atomic compound updates
- Leases and authoritative deadlines
- Idempotency fingerprints
- Money versus inventory outcomes
Scope and explicit decisions
- In-memory inventory across configured shows and named seats. Each seat costs one configured positive minor-unit price; show-seat identity matters. Search, seat-map rendering, real payment processing, authentication, durable storage and a waiting room are not implemented.
- A hold is all-or-nothing over a nonempty seat set. HELD and CONFIRMED both own their seats. A HELD record expires when expiresAt<=trusted now. CONFIRMED ownership does not expire.
- Time is a nonnegative, nondecreasing elapsed-unit value supplied by a trusted adapter. New operations and duplicate holds advance time and sweep all expired HELD records. A read through get shows the last processed state and does not itself advance time.
- TTL and total price use checked arithmetic before allocation. A valid operation that advances time may expire unrelated old holds even if its requested seats remain unavailable; rejection does not undo the passage of time.
- hold keys are scoped to the inventory and fingerprint show, seat set, owner and TTL. Same key resolves the same hold identity; a retry cannot extend its deadline or create a new hold after expiry. Set iteration order does not change identity.
- confirm accepts an authenticated completed-payment fact from a trusted adapter, not an unverified browser receipt. Owner and amount must match the stored hold. The implementation does not actually charge or refund money.
- A first valid payment confirms only a still-HELD record. Payment after EXPIRED/CANCELLED, or another distinct successful payment after CONFIRMED, records REFUND_REQUIRED without changing seat ownership. A matching duplicate payment returns its stored result; conflicting reuse fails.
- cancel releases an unconfirmed HELD record or returns its existing terminal state. Confirmed booking cancellation requires a separate refund policy/workflow and is rejected here. A refund obligation is a recorded result, not proof the external refund occurred.
- Hold, command and payment histories remain in memory for the process lifetime. Restart loses ownership and dedupe state; a durable multi-process adapter is a Staff design extension.
Worked examples
Input: Alice holds A,B until t=10. Bob asks for B,C before t=10.
Outcome: Bob is rejected with no claim on C. At t=10 Alice expires and Bob may hold A.
Atomic group allocation and exact deadline semantics are separate invariants.
Input: Alice’s successful charge arrives after Bob owns A.
Outcome: Alice payment records REFUND_REQUIRED; Bob’s ownership is unchanged.
Financial repair is required instead of retroactively winning the seat race.
Try before revealing
Hint 1
What identity owns a seat between selection and confirmed booking?
Hint 2
Does the exact deadline belong in a timer or in every authoritative decision?
Hint 3
What must happen to a successful charge that loses the seat race?
Requirements and invariants
R1 — Allocate every requested show-seat or none, with at most one owner per seat. hold protects I1.
R2 — Expiry and cancellation release only seats still owned by that hold. release protects I2.
R3 — Late or duplicate successful charges cannot acquire another owner’s seats. confirm protects I3.
I1: Each HELD/CONFIRMED seat maps to exactly its hold ID; no seat has two active owners.
I2: Terminal unconfirmed holds own no seats. Conditional release cannot remove a later hold’s ownership.
I3: Each payment ID has one immutable request fingerprint and one stored confirmation/refund outcome.
Derive the design
Availability display is not an allocation decision
A seat-map reader can show which seats appear available, but read-then-write from independent callers permits both to observe the same free seat. Start with a single atomic service decision: validate the whole group and claim it under one owner. The reference adds time-limited holds and immutable IDs before payment integration so scarce ownership remains authoritative.
Alternative: Per-seat locks can permit unrelated seats to progress, but group claims need deterministic lock ordering and shared hold/payment records. A database conditional update or transaction can coordinate multiple processes; a JVM lock cannot. A waiting room reduces pressure but does not replace ownership checks.
Clarify ownership and expiry before payments
Ask about show-seat identity, group atomicity, time authority and exact deadline. A payment API cannot determine correct booking behavior until those contracts are fixed.
Make one local transaction easy to inspect
The monitor encloses expiry, group validation, ownership creation and command-result recording. It never calls a payment provider.
Separate repeat identity from fresh business intent
An expired hold key still identifies that old hold. A new attempt to reserve seats needs a new key; otherwise a retry could silently extend a lease.
Record the losing payment path
A late payment must have an explicit result that can drive refund/review work. Keeping this path visible makes the design maintainable under reordered callbacks.
Responsibilities and ownership
Inventory
Owns: Configured show-seats, ownership, hold history and operation/payment results
Methods: hold, confirm, cancel, ownerOf
One monitor owns allocation, expiry and payment-to-seat decision.
Hold
Owns: Stable identity, immutable seat set/owner/price/deadline and lifecycle snapshot
Methods: Record accessors
Callers cannot extend TTL or set CONFIRMED themselves.
Seat / Request / Payment
Owns: Structured identity and retry arguments
Methods: Immutable record equality
Show-seat keys and set-based fingerprints avoid accidental cross-show conflicts or ordering differences.
Confirmation
Owns: Payment correlation and BOOKED/REFUND_REQUIRED outcome
Methods: Immutable record accessors
A repair obligation is not an external refund acknowledgement.
Payment I/O is outside this local model; confirmation input is trusted completed-payment evidence.
- Show-seat group + owner + key
↓ Trusted clock and checked price
Enforce expiry and validate all seats↓ No partial group mutation
Claim one immutable hold↓ Payment success cannot override expiry
Confirm current ownership or record repair
Refund-required is an obligation to an external workflow, not a completed refund.
Current exclusive owner
Preserve existing seat ownership
Data model, schema and relationships
BookMyShow needs a reusable physical seat and a separate per-show seat inventory. Users place orders for a particular show; order lines connect the order to its selected seats. Payment attempts and hold ownership are separate facts.
Implementation boundary: The reference implements per-show atomic holds and correlated payment outcomes in memory. User, movie, theatre and screen catalog tables below explain the surrounding domain; SQL constraints/transactions are a proposed durable extension.
These are proposed logical schemas with field contracts, not executable database migrations. PK identifies a row, FK references an existing parent, UK is a uniqueness rule. A composite PK identifies the combination. Nullable references are marked with ?. A foreign key protects identity; cross-row limits, timing and state transitions also require the transaction rules below.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Entity definitions and field contracts
app_user
Purchaser identity.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Account identity |
email | text · UK | Login contact |
Code mapping: External user adapter.
- Normalize account identifiers; authorization must bind order owner to authenticated caller.
movie
Movie catalog.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Movie identity |
title | text · VALUE | Display title |
duration_minutes | integer · VALUE | Runtime |
Code mapping: Catalog extension.
- A movie has many scheduled shows; title is not unique identity.
theatre
Cinema location.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Theatre identity |
name | text · VALUE | Display label |
timezone | text · VALUE | IANA display/scheduling zone |
Code mapping: Catalog extension.
- Persist show instants; timezone is for presentation and schedule interpretation.
screen
Auditorium within theatre.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Screen identity |
theatre_id | uuid · FK | Location owner · theatre.id |
name | text · VALUE | Auditorium label |
Code mapping: Physical screen catalog extension.
- Unique(theatre_id,name); prevent overlapping shows via a separate scheduling constraint.
seat
Reusable physical seat.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Physical seat identity |
screen_id | uuid · FK | Auditorium owner · screen.id |
label | text · VALUE | Row/number |
category | text · VALUE | Pricing category |
Code mapping: Seat identifier input; screen metadata extension.
- Unique(screen_id,label). A seat is not globally sold forever.
show
One screening occurrence.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Show identity |
movie_id | uuid · FK | Screened movie · movie.id |
screen_id | uuid · FK | Assigned auditorium · screen.id |
starts_at | timestamp · VALUE | Absolute start instant |
Code mapping: Show-scoped booking owner.
- No conflicting screen time interval; include runtime/cleanup end in real scheduling.
booking_order
One user purchase/hold lifecycle.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Order identity |
user_id | uuid · FK | Purchaser · app_user.id |
show_id | uuid · FK | Screening · show.id |
state | enum · VALUE | HELD CONFIRMED EXPIRED etc |
expires_at | timestamp · VALUE | Hold deadline |
created_at | timestamp · VALUE | Immutable acceptance instant for history queries |
Code mapping: Hold identity/state; account metadata extension.
- One order references one show; successful request key binds user/show/exact seat set.
show_seat
Current ownership for one screening seat.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Show-seat surrogate identity |
show_id | uuid · FK | Screening owner · show.id |
seat_id | uuid · FK | Physical seat · seat.id |
owner_order_id | uuid? · FK | Nullable current hold/sale owner · booking_order.id |
version | bigint · VALUE | Conditional ownership version |
Code mapping: Per-show seat ownership map; current relation changes over time.
- Unique(show_id,seat_id); seat.screen_id must equal show.screen_id, enforced by composite keys/transaction validation. Current owner must belong to same show.
order_seat
Historical order line with immutable price.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Line identity |
order_id | uuid · FK | Purchase owner · booking_order.id |
show_seat_id | uuid · FK | Selected show-seat · show_seat.id |
price | bigint · VALUE | Snapshotted minor-unit price |
Code mapping: Held seat set and trusted price context.
- Unique(order_id,show_seat_id); history can include expired orders, so do not globally unique show_seat_id here.
payment_attempt
Provider charge evidence and repair status.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | text · PK | Stable provider payment identity |
order_id | uuid · FK | Correlated purchase · booking_order.id |
amount | bigint · VALUE | Expected exact amount |
outcome | enum · VALUE | Unknown accepted or refund-required |
currency | text · VALUE | Charge currency checked against immutable order prices |
Code mapping: Payment outcome ledger; real refund executor is external.
- Provider identity unique; verify currency/amount/order; distinct extra successful charges require separate refund obligations.
Relationship cardinalities and their reasons
| Parent → child | Multiplicity | FK / why |
|---|---|---|
| theatre → screen contains screens | 1 parent per child; 0..* children per parent | screen.theatre_id → theatre.idA theatre contains many auditoriums. |
| screen → seat contains seats | 1 parent per child; 0..* children per parent | seat.screen_id → screen.idPhysical seats belong to exactly one auditorium. |
| movie → show is screened | 1 parent per child; 0..* children per parent | show.movie_id → movie.idA movie has many show occurrences. |
| screen → show hosts shows | 1 parent per child; 0..* children per parent | show.screen_id → screen.idThe same screen hosts different non-overlapping occurrences. |
| app_user → booking_order places orders | 1 parent per child; 0..* children per parent | booking_order.user_id → app_user.idA user may have many orders; ownership authorizes access. |
| show → booking_order receives bookings | 1 parent per child; 0..* children per parent | booking_order.show_id → show.idAn order buys seats for exactly one show. |
| show → show_seat owns inventory | 1 parent per child; 0..* children per parent | show_seat.show_id → show.idEach show materializes its own independent seat availability. |
| seat → show_seat is reused across shows | 1 parent per child; 0..* children per parent | show_seat.seat_id → seat.idPhysical A12 can be sold once per show, rather than once globally. |
| booking_order → show_seat currently owns | 0..1 parent per child; 0..* children per parent | show_seat.owner_order_id → booking_order.idA show-seat has zero or one current order; that order can own many seats. Nullable owner distinguishes available inventory. |
| booking_order → order_seat has price lines | 1 parent per child; 0..* children per parent | order_seat.order_id → booking_order.idOrder lines preserve selected seats/prices after expiry or completion. |
| show_seat → order_seat appears in history | 1 parent per child; 0..* children per parent | order_seat.show_seat_id → show_seat.idMany historical order lines may reference one show-seat; current ownership is constrained elsewhere. |
| booking_order → payment_attempt has payment evidence | 1 parent per child; 0..* children per parent | payment_attempt.order_id → booking_order.idAt most one charge fulfills the order; extra accepted charges need repair tracking. |
Transaction boundaries
- Hold: lock requested show_seat rows in sorted identity order, expire eligible prior ownership, validate every seat belongs to the show and is available, then insert order/lines and claim all rows atomically. Never expose half a requested seat group.
- Confirm: resolve provider identity, verify owner/show/amount/currency and deadline, then mark order and all still-owned seats confirmed together. A late charge becomes refund-required without stealing seats from a newer order.
- Expire: compare current owner order plus HELD state/deadline; release only matching claims. Physical/provider I/O occurs outside this transaction and uses durable outbox/reconciliation in an extension.
Indexes and uniqueness
- Unique show_seat(show_id,seat_id), seat(screen_id,label); index show(movie_id,starts_at) and show(screen_id,starts_at).
- Index booking_order(user_id,created_at), active booking_order(expires_at), order_seat(order_id), payment_attempt(order_id).
- Unique scoped idempotency key with request fingerprint; lock order and current show-seat ownership in one transaction.
Schema traps and failure cases
- A foreign key alone cannot enforce seat.screen_id=show.screen_id or owner order.show_id=inventory.show_id; use composite keys or guarded transaction validation.
- Deleting expired order history would erase payment/refund evidence. Keep historical lines and clear only conditional current ownership.
- Do not implement active uniqueness with a time-dependent now() index predicate; store state/owner explicitly and advance expiry transactionally.
Trace the records through one operation
- User U selects show SH1 in screen SC1 and seats A1,A2; SH2 has separate show_seat rows for the same physical seats.
- Order O1 atomically holds SH1/A1 and SH1/A2; order_seat stores price snapshots.
- O1 expires and O2 claims A1. A late O1 payment records refund-required and cannot change O2 ownership.
Method contracts
hold(show,seats,owner,ttl,key,now) → Hold
Validate identifiers/configuration and checked price/deadline. Sweep expiry, inspect all seats, then claim the whole set and record command identity atomically.
confirm(holdId,owner,paymentId,amount,now) → Confirmation
Resolve matching payment duplicates before new mutation. For a fresh payment validate owner/amount, sweep expiry and confirm only HELD; otherwise record refund obligation.
cancel(holdId,owner,now) → Hold
Enforce owner and current expiry. Release only HELD seats through identity-conditional removal. Reject confirmed cancellation pending a separate policy.
ownerOf(show,seat,now); get(holdId)
ownerOf advances trusted time and sweeps expired records. get returns the latest stored snapshot without inventing a new clock observation.
invariantHolds() → boolean
Diagnostic recount of active hold seats versus ownership map, used in correctness tests rather than needed in every hot-path call.
What, when, why and how
Each concept is attached to the requirement it protects, its implementation, an alternative and a test.
Atomic scarce-resource ownership
- what
- Commit one exclusive claim for every seat in a requested group.
- when
- Two customers or gates compete for overlapping seat sets.
- why
- Partial group claims or independent checks can create surprise split bookings and double sales.
- how
- hold validates all show-seat keys before inserting any ownership. The synchronized boundary also creates hold and dedupe result.
Code: hold, Seat · Invariants: I1
Alternatives, cost and failure example
Alternative: Ordered per-seat locks or a transactional store are alternatives; neither removes the need for one all-or-nothing group contract.
Cost: For K seats, validation/insertion is O(K) expected map work after the expiry scan; the whole service currently serializes.
Counterexample: Bob asks for B,C while Alice holds B; naïvely claiming C first leaves Bob with an unwanted partial group.
Test: Reject the overlapping group and assert C remains free; race identical groups and assert one winner.
Lease deadline enforced by the authoritative owner
- what
- A hold is exclusive only until a declared instant.
- when
- Customers abandon checkout or payment arrives late.
- why
- A timer callback can be late; correctness cannot depend on cleanup executing exactly at the deadline.
- how
- at(now) sweeps HELD records with expiresAt<=now before relevant ownership decisions. Deadline is immutable; confirmation rechecks after sweeping.
Code: at, expire, confirm · Invariants: I1, I2
Alternatives, cost and failure example
Alternative: A background expiry index improves cleanup cost, but reads/writes still need authoritative deadline enforcement.
Cost: The simple sweep scans H retained holds per clock-advancing call; an expiry heap/index is a measured optimization.
Counterexample: A delayed expiry task allows payment at the exact expired boundary to steal a seat already promised to another owner.
Test: At t=deadline the hold is expired, a new hold may claim the seat, and the old payment becomes REFUND_REQUIRED.
Idempotency and identity-conditional release
- what
- Resolve repeated logical operations and never release a resource owned by a newer identity.
- when
- Clients retry or old cleanup events arrive after seat reuse.
- why
- Deduping only by payload or deleting ownership by seat alone can corrupt the newer booking.
- how
- Structured Request retains show/set/owner/TTL. Payments retain hold/owner/amount. release uses ownership.remove(key,oldHoldId), and terminal cancel does not release again.
Code: Request, Payment, release, cancel · Invariants: I2, I3
Alternatives, cost and failure example
Alternative: Durable dedupe results must share the transaction with ownership changes; an evictable side cache is insufficient.
Cost: History retention grows; expiry of business records needs an explicit retry/reconciliation horizon.
Counterexample: Old Alice cancellation clears seat A even though it now belongs to Bob.
Test: Expire/reallocate, then retry old cancel and verify Bob remains owner; reject payment-ID reuse for another hold.
Payment outcome versus inventory outcome
- what
- A successful charge is a fact to reconcile, not permission to rewrite scarce ownership.
- when
- Money success arrives after cancellation/expiry or twice under distinct payment IDs.
- why
- Confirming blindly would double-sell; ignoring the charge would lose the customer’s financial repair obligation.
- how
- confirm records BOOKED only for HELD. All other fresh successful-charge cases yield REFUND_REQUIRED and preserve inventory.
Code: Confirmation, confirm · Invariants: I1, I3
Alternatives, cost and failure example
Alternative: Authorize before final capture where provider semantics support it, but define timeout/expiry and capture races explicitly.
Cost: Refund processing needs its own durable state, retries and audit; this local result does not execute it.
Counterexample: A late charge changes EXPIRED→CONFIRMED even after another customer acquired the same seat.
Test: Confirm an expired hold after reallocation and a second distinct payment on an already confirmed hold; both require refund without ownership change.
Read and run the Java reference
Reveal complete Solution.java
Java 21. Save as Solution.java; compile with javac --release 21. The repository also contains Test.java and the lld:java verification command. This reference implements the local contract above.
hold prepares arithmetic and checks the full group
Compute deadline and total amount with overflow checks, then inspect every seat before any claim. A validation failure cannot create half a group.
expire releases through hold identity
An expired record becomes EXPIRED and its seats are conditionally removed. This protects new owners even if old repair calls repeat.
confirm stores the financial decision
Fresh payment identity and current hold state determine BOOKED versus REFUND_REQUIRED. Matching duplicate payment returns its original outcome instead of re-running ownership logic.
Complexity and limits
- A fresh hold/confirm/cancel advances time and scans H retained holds for expiry: O(H+K) expected work for K group seats, plus checked fixed-width amount arithmetic.
- ownerOf also sweeps expiry; it is not a free O(1) read in this implementation. get is expected O(1) without clock advancement. invariantHolds recounts all active seat memberships.
- Space is O(configured seats + active ownership + retained hold/operation/payment histories). History is unbounded over process lifetime; no persistence or retention cleanup is implemented.
- A single monitor provides local atomicity but serializes unrelated shows. Per-show partitioning is a plausible next boundary if payment/hold identities and cross-show groups are explicitly scoped.
Walk through the actual reference
These walkthrough fixtures are emitted only after the Java harness verifies their operations and expected state. Concurrency tests additionally exercise competing callers.
Group hold and exact expiry
Step 1 of 3: Alice holds A,B until t=10
Both owned by one hold; price 200
Invariant: All-or-nothing group allocation
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Alice holds A,B until t=10 | Both owned by one hold; price 200 | All-or-nothing group allocation |
| Bob asks for B,C before expiry | Rejected; C remains free | No partial claim |
| At t=10 Bob holds A | Alice expired; Bob owns A | Expiry is enforced by authoritative operations |
Late payment does not steal a reallocated seat
Step 1 of 2: Alice payment arrives after expiry
REFUND_REQUIRED; Bob still owns A
Invariant: Money success cannot override seat ownership
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Alice payment arrives after expiry | REFUND_REQUIRED; Bob still owns A | Money success cannot override seat ownership |
| Retry old cancellation | No release of Bob hold | Release is conditional on original hold identity |
Mistakes and counterexamples
Trust a stale seat-map read
Failing case: Two customers both saw A free.
Repair: Recheck and mutate atomically at the ownership boundary.
Make background TTL cleanup authoritative
Failing case: Expiry task runs late.
Repair: Enforce expiresAt during authoritative operations.
Release by seat without checking owner
Failing case: Old hold cleanup runs after reallocation.
Repair: Use stable hold identity and terminal-state guards.
Treat refund-required as refunded
Failing case: The service returned a repair status but provider refund failed.
Repair: Track external compensation separately with its own identity and evidence.
Engineer → Senior → Staff
Implement precise seat/hold behavior.
Executed: all-or-nothing groups, show-seat identity, checked price, expiry and basic confirmation.
Change exercise
Add the same seat label to a second show and demonstrate independence.
Worked answer guidance
Use (show,seat) ownership keys. A raw seat label alone incorrectly links inventory across separate screenings.
Interview follow-ups
Why does get not advance expiry?
It has no trusted now argument. It reports the latest processed state; operations with a time observation perform enforcement.
Why is TTL part of the hold request fingerprint?
Changing TTL changes the requested lease. Same-key retries cannot quietly extend it.
Your design attempt
Each language has a separate draft across all three levels. Saving is device-only and does not sync to your account. Anyone using this browser profile can load it. Loading is always explicit.
Java 21 reference code is provided for local study. Use Run program in the editor to execute your code and test driver. Program runs show output; they do not grade your design.
Run program · Java 21
No unsaved changes · 208/20000
Self-review
Checks apply to this visit. Self-review is not a passed submission or verified mastery.
Provenance and scope of evidence
Original teaching material. The local contract is implemented by Solution.java and tested by Test.java on Java 21. Distributed persistence, real provider/device adapters and deployment are not claimed.
Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.