LLD-001 · LOW LEVEL DESIGN
Parking Lot
From a simple spot scan to atomic allocation, immutable ticket terms and safe duplicate operations.
Local Java 21 reference verified: 30 checks. Learner drafts are not executed. Author self-review; independent review pending.
Existing introductory lesson · Version 1
Understand the problem
Design a parking facility with several entry and exit gates. Allocate a compatible spot, issue a ticket, calculate a fee, record a trusted payment receipt and release occupancy only when the vehicle exits. Explain which object owns each invariant and why a thread-safe map alone cannot make allocation safe.
Before you start
- Objects and responsibilities
- SOLID and composition
- Concurrency and correctness
Scope and explicit decisions
- One process and one facility. No database, hardware gate, license-plate recognition or payment-provider network calls are implemented.
- Amounts are nonnegative integer minor units in one facility currency. Pricing rounds positive partial hours up; zero elapsed time costs zero. No floating-point arithmetic.
- SMALL vehicles can use either spot size; LARGE vehicles require LARGE. First compatible spot in configured order wins. This simple rule may consume a large spot that a later large vehicle needs.
- The trusted caller supplies nonnegative elapsed seconds from one clock origin. An earlier quote time is rejected. Billing-clock selection is an adapter responsibility.
- A ticket captures the immutable Rate at entry. Rate changes affect new tickets only. A quote is immutable; paying any issued quote freezes that charge. There is no post-payment grace deadline in this teaching contract.
- The payment boundary is trusted: receipt strings are deduplication identifiers, not proof of a genuine payment. A production adapter must authenticate and validate currency/amount against the quote before calling recordPayment.
- Operation IDs are facility-scoped and never reused. Retrying park returns the same ticket identity with its current state. Reusing a key for a different vehicle is rejected.
- Parking records, quotes, receipts and idempotency results remain in memory for this reference. Storage is unbounded over lifetime; restart loses all state. Retention and durability are explicitly Staff design extensions.
Worked examples
Input: One LARGE spot. Vehicle A enters at 0; quote at 3600; rate 100/hour.
Outcome: One OPEN ticket; quote = 100. Payment → PAID with occupancy 1. Exit → CLOSED with occupancy 0.
Billing and physical allocation have different completion points.
Input: A enters at rate v1=100. Install v2=200. A leaves after one hour; B enters under v2.
Outcome: A quote stays 100; B one-hour quote is 200.
Historical behavior follows captured policy, not current configuration.
Try before revealing
Hint 1
Which operation must create both occupancy and ticket ownership together?
Hint 2
Draw OPEN, PAID and CLOSED. Which transition actually means the vehicle has left?
Hint 3
Keep immutable pricing on the ticket and a command-result ledger inside the same atomic boundary.
Requirements and invariants
R1 — At most one active ticket owns any compatible spot. park protects I1.
R2 — Payment changes ticket state but not occupancy. recordPayment protects I2.
R3 — Only paid tickets can exit; repeated old exits cannot release a new occupant. exit protects I1.
R4 — Existing tickets retain their entry pricing version. changeRate protects I3.
I1: Every OPEN/PAID ticket has exactly one occupied spot and one active vehicle entry, and vice versa.
I2: Allowed lifecycle: OPEN → PAID → CLOSED. Only CLOSED releases the spot.
I3: Ticket.rate never changes after creation, even if currentRate changes.
Derive the design
A correct simple scan, then one explicit synchronization boundary
Start with a single-threaded list scan: choose a compatible free spot, create a ticket, mark occupancy. For one command at a time this is a complete allocation mechanism. Do not optimize before identifying the invariant. The reference keeps the scan and adds synchronized service operations, preserving a clear atomic check-and-update. A ConcurrentHashMap only makes each map call safe; the decision spans several maps. A first-fit scan is not a broken baseline—it is an intentional O(S) choice for a small facility.
Alternative: If measured allocation latency is dominated by scanning, maintain free-spot indexes by compatibility class. Removing an index entry and creating a ticket must still share the same transaction/lock. Per-floor locks increase parallelism but cross-floor selection and migration introduce ordering concerns.
Identify the hard invariant before naming classes
Vehicle and Spot are nouns, but the crucial behavior is a single ownership decision. Ticket cannot safely claim a spot independently of the allocation service. Keep a single authoritative map relationship, then expose immutable snapshots.
Separate payment acknowledgement from physical exit
A vehicle may remain inside after paying. Releasing on payment would permit a new ticket for an occupied spot. OPEN→PAID preserves occupancy; PAID→CLOSED releases it. A hardware gate needs a separate authenticated event contract in a real implementation.
Choose a lock that covers the real transaction
One monitor provides visibility and mutual exclusion for selection, ticket creation and duplicate recording. A synchronized getter is also needed for consistent reads. Only after measuring contention should this become indexed allocation or per-floor locking.
Decide retries at the command boundary
Store enough of the request to detect conflicting key reuse. Here immutable Vehicle equality is the fingerprint; at is treated as observed execution time, not part of request identity. Specify this distinction so a retry does not fail merely because its clock is later.
Responsibilities and ownership
ParkingLot
Owns: Spot ownership, ticket lifecycle, operation and receipt ledgers
Methods: park, quote, recordPayment, exit
One monitor owns every compound state change; it performs no network I/O.
Vehicle / Spot
Owns: Validated identity and compatibility class
Methods: Immutable Java record accessors
Do not own allocation or payment.
Ticket
Owns: Entry time, captured rate, vehicle/spot identity and lifecycle snapshot
Methods: Immutable accessors; service replaces snapshot
Callers cannot set state directly.
Rate / Quote
Owns: Versioned price rule / immutable computed amount
Methods: Validated constructors and accessors
Rate is a concrete immutable policy value, not an arbitrary callback under the lock.
The monitor encloses only local state changes. Provider I/O is outside this model.
- Gate / trusted adapter
↓ Gate invokes park / payment / exit
ParkingLot monitor↓ One monitor commits all related maps
Occupancy + ticket maps↓ Caller receives immutable state
Immutable ticket snapshot
OPEN cannot exit. Repeated payment/exit resolves the existing identity; CLOSED never releases occupancy again.
- OPEN: owns spot
↓ Trusted receipt for stored quote
PAID: owns spot↓ Confirmed physical exit
CLOSED: releases spot
Data model, schema and relationships
Parking identity is separate from physical occupancy, ticket lifecycle and payment evidence. A vehicle may visit many times; a spot is reused over time, so historical tickets cannot uniquely own it forever.
Implementation boundary: The reference uses local Spot/Ticket records and maps. These tables are a proposed persistence design; vehicle/payment rows expand the adapter boundary rather than claim a database already exists.
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.
Entity definitions and field contracts
vehicle
Stable vehicle identity across visits.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Vehicle identity |
plate | text · UK | Normalized registration within issuing jurisdiction |
kind | enum · VALUE | Compatibility class |
jurisdiction | text · VALUE | Plate issuing authority used in normalized uniqueness |
Code mapping: Vehicle compatibility input.
- Unique (jurisdiction,normalized_plate) in a real multi-region system; plate alone is not globally unique.
spot
Physical capacity owned by one facility.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Spot identity |
kind | enum · VALUE | Supported vehicle class |
version | bigint · VALUE | Conditional allocation version |
Code mapping: Spot record and occupancy map.
- Exactly one current occupancy per spot; historical ticket references are allowed.
ticket
One visit with snapshotted pricing policy.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Visit identity |
vehicle_id | uuid · FK | Visiting vehicle · vehicle.id |
spot_id | uuid · FK | Allocated spot · spot.id |
state | enum · VALUE | OPEN or CLOSED |
policy_version | text · VALUE | Original pricing interpretation |
created_at | timestamp · VALUE | Immutable acceptance instant for history queries |
Code mapping: Ticket and atomic park/leave.
- Unique active ticket per vehicle and spot, enforced by partial unique indexes or explicit occupancy rows.
payment
Trusted correlated payment evidence.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | text · PK | Provider operation identity |
ticket_id | uuid · FK | Visit being settled · ticket.id |
amount | bigint · VALUE | Exact minor units |
status | enum · VALUE | Known or unknown provider result |
currency | text · VALUE | Currency must match ticket pricing contract |
Code mapping: Payment adapter extension; reference trusts payment input.
- Provider identity unique; validate amount/currency and ticket association before release.
Relationship cardinalities and their reasons
| Parent → child | Multiplicity | FK / why |
|---|---|---|
| vehicle → ticket makes visits | 1 parent per child; 0..* children per parent | ticket.vehicle_id → vehicle.idOne vehicle has many historical visits; active-visit uniqueness is a separate constraint. |
| spot → ticket hosts visits | 1 parent per child; 0..* children per parent | ticket.spot_id → spot.idA spot has many historical tickets, but at most one active owner. |
| ticket → payment has settlement evidence | 1 parent per child; 0..* children per parent | payment.ticket_id → ticket.idMultiple attempts may exist; one accepted settlement must not free a spot twice. |
Transaction boundaries
- park: lock/conditionally claim compatible spot, enforce vehicle active uniqueness and insert ticket in one transaction.
- leave: correlate accepted settlement with the same ticket, close it and release its current occupancy atomically. Provider I/O occurs outside database locks.
Indexes and uniqueness
- Unique active (spot_id) and active (vehicle_id); ordinary historical FKs stay nonunique.
- Index ticket(vehicle_id,created_at) for visit history and payment(ticket_id,status) for settlement lookup.
Schema traps and failure cases
- A stale ticket cannot release a spot now owned by another visit.
- Unknown payment is retained for reconciliation; it is not definite failure.
Trace the records through one operation
- Vehicle V opens T1 in S1; active uniqueness blocks a second owner.
- Settlement P1 closes T1 and frees S1 in one commit.
- V returns under T2; replaying T1/P1 cannot release T2.
Method contracts
park(Vehicle, operationId, at) → Ticket
Validates input, finds first compatible spot, claims it and records ticket/result under one monitor. Full/already parked are rejected without mutation. Retry key must match the vehicle; the later retry time is not a new business request.
quote(ticketId, at) → Quote
OPEN only. Reject clock reversal. Use checked multiplication; overflow fails before quote storage. Quote creation does not charge money.
recordPayment(quoteId, receiptId) → Ticket
Trusted adapter only. OPEN → PAID. Same receipt+quote is a no-op returning current ticket; receipt reused for another quote fails.
exit(ticketId) → Ticket
PAID → CLOSED with occupancy release. OPEN fails; CLOSED is a no-op, including after the spot has a new occupant.
changeRate(Rate) → void
Replaces the current entry policy under the same monitor. Existing records retain their Rate object.
What, when, why and how
Each concept is attached to the requirement it protects, its implementation, an alternative and a test.
Encapsulation and immutable snapshots
- what
- Put state transitions behind methods and return immutable records.
- when
- When callers could otherwise edit a status, occupied flag or rate.
- why
- A valid ticket and its occupancy must change together; a public status setter bypasses that invariant.
- how
- ParkingLot keeps private maps. withState returns a new Ticket. The caller sees a snapshot; later payment does not mutate an older Ticket reference.
Code: withState, ticket · Invariants: I1, I2
Alternatives, cost and failure example
Alternative: Public mutable DTOs are simpler only for unshared input, not authoritative domain state.
Cost: New snapshots allocate objects; records retain referenced values, so all nested values here are immutable.
Counterexample: Caller changes ticket to CLOSED but the spot remains occupied: a public setter creates contradictory truth.
Test: Attempt illegal exit and assert state/occupancy are unchanged; snapshot rate remains v1.
Atomic aggregate update
- what
- Protect the entire read-decide-write operation, not individual fields.
- when
- Two entry gates compete for the final compatible spot.
- why
- Both callers can read free before either writes, producing two active tickets for one spot.
- how
- synchronized park selects the spot, updates occupied/tickets/activeVehicles and stores the operation result while holding the same monitor. All mutating readers use that monitor too.
Code: park, invariantHolds · Invariants: I1
Alternatives, cost and failure example
Alternative: One service event loop can also serialize commands. A database transaction is required when owners span processes.
Cost: All facility operations serialize. The lock includes only bounded in-memory work, never provider I/O.
Counterexample: Check occupied outside the monitor then write inside: both gates can pass the stale check.
Test: Release two workers from a CountDownLatch; assert exactly one winner and the map/ticket invariant.
Money and versioned pricing policy
- what
- Represent amounts exactly and bind a policy version to the business record.
- when
- Fees must remain explainable after price changes.
- why
- Re-reading currentRate for an old ticket silently changes the agreement made at entry.
- how
- Rate is an immutable version and minorUnitsPerHour. Ticket captures it. quote computes ceiling elapsed hours and Math.multiplyExact; a zero-duration visit costs zero.
Code: Rate, quote, changeRate · Invariants: I3
Alternatives, cost and failure example
Alternative: For variable tariffs introduce a pure PricingPolicy outside network boundaries, with immutable versioned inputs. Do not add a Strategy interface solely to increase class count.
Cost: Whole-hour rounding is a business rule, not numerical necessity; complex tariffs need more metadata and tests.
Counterexample: At 3601 seconds, truncating elapsed/3600 undercharges by one hour; using doubles risks exact receipt comparison.
Test: Test 0, 3600 and 3601 seconds, overflow, and v1 ticket after v2 rollout.
Idempotent command identity
- what
- A retry resolves the original logical operation instead of performing it again.
- when
- Gate/client times out after an allocation or payment was accepted.
- why
- A retry must not allocate another ticket or release another car’s reused spot.
- how
- Store operationId→vehicle+ticket and receiptId→quote. Reject conflicting reuse. exit uses the CLOSED ticket as its natural deduplication state.
Code: park, recordPayment, exit · Invariants: I1, I2
Alternatives, cost and failure example
Alternative: If durable storage is introduced, put the dedupe record and business mutation in the same transaction. A separate cache can lose the atomic relationship.
Cost: Retained results grow over time; retention expiry must not allow ancient retries to become new commands.
Counterexample: Delete a closed ticket then retry old exit after S1 is reallocated: naïve release-by-spot can evict the new owner.
Test: Repeat payment and exit; reallocate S1; retry old exit and assert new occupancy remains.
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.
park: retry before allocation
Resolve an existing operation first, then reject another active ticket for the vehicle. Select a compatible spot while still owning the monitor; commit all maps before returning. No caller can observe a partial committed state through the public methods.
quote: calculate before storing
Validate state and time, derive ceil-hours without adding 3599 (which could overflow), then multiply with overflow checking. Only after a valid amount exists do we allocate/store the Quote.
recordPayment and exit: monotonic state
Receipt identity prevents another mutation on retry. An old CLOSED ticket exits as a no-op before touching occupancy. This order is why stale exits cannot free a new owner’s spot.
changeRate: immutable policy replacement
New tickets capture the new Rate. Existing Tickets point to their earlier immutable Rate, so no data migration is needed for this particular change.
Complexity and limits
- park scans S spots: O(S) expected map lookup work; ticket/receipt lookup and lifecycle changes are expected O(1). Hash lookup is not an unconditional worst-case guarantee.
- quote is O(1) for fixed-width arithmetic; checked multiplication throws on overflow. invariantHolds is O(T) and is a diagnostic/test operation, not a hot-path necessity.
- Configured spots use O(S); retained tickets T, quotes Q, operations O and receipts R use O(T+Q+O+R). This example has no retention cleanup or durability.
- One monitor serializes all commands. O(1) data access does not imply bounded latency under contention; queueing grows with arrival rate.
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.
Last spot, payment and physical exit
Step 1 of 5: A parks
S1 owned by A; ticket OPEN
Invariant: 1 active ticket = 1 occupied spot
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| A parks | S1 owned by A; ticket OPEN | 1 active ticket = 1 occupied spot |
| B attempts the last spot | Rejected: full; A still owns S1 | No double allocation |
| Quote at 3600 seconds | 100 minor units at v1 | Ticket retains its original rate |
| Record trusted payment receipt | Ticket PAID; S1 remains occupied | Payment does not release physical occupancy |
| Confirm exit | Ticket CLOSED; S1 free | 0 active tickets = 0 occupied spots |
Mistakes and counterexamples
Use a concurrent map and assume the whole operation is safe
Failing case: Gate A and B both observe S1 free before either inserts.
Repair: Synchronize selection plus all ownership writes and idempotency recording.
Release the spot when the payment succeeds
Failing case: A pays but has not driven out; B receives A’s occupied spot.
Repair: Keep PAID active; release only on confirmed exit.
Calculate old ticket prices from current configuration
Failing case: v2 doubles the hourly price while A is already inside.
Repair: Capture an immutable policy on ticket creation and keep it in quotes.
Treat a fake receipt string as proof of payment
Failing case: An untrusted client invents receipt-123 and calls recordPayment.
Repair: Authenticate provider events and compare amount/currency in a trusted adapter before this domain API.
Engineer → Senior → Staff
Derive a small, working object model.
Executed: first-fit compatible allocation, immutable values, rounding and valid lifecycle.
Change exercise
Add a SMALL vehicle and two differently sized spots; explain allocation order and write the zero-duration pricing test.
Worked answer guidance
First-fit is deterministic but not future-optimal. Preserve narrow domain responsibilities and show why 0 elapsed seconds produces a zero quote under this contract.
Interview follow-ups
Why is Ticket a record rather than a public mutable class?
It gives callers a snapshot while ParkingLot owns transitions. A record alone is not deep immutability if nested values are mutable; these nested values are records/enums.
Why not create a subclass for every kind of vehicle?
Compatibility is a small value-based rule. A hierarchy is only justified by genuinely different substitutable behavior, not by nouns.
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 · 187/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. Local semantics are defined by this chapter and executable Solution.java/Test.java. Java 21 language/runtime is pinned by the verification harness.
Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.