All chapters in this track
- 01Chapter 1: Why Object Orientation Exists
- 02Chapter 2: Classes, Objects, and Instances, Precisely Defined
- 03Chapter 3: The Four Pillars I: Encapsulation
- 04Chapter 4: The Four Pillars II: Abstraction
- 05Chapter 5: The Four Pillars III: Inheritance
- 06Chapter 6: The Four Pillars IV: Polymorphism
- 07Chapter 7: Composition vs. Inheritance
- 08Chapter 8: Interfaces and Abstract Classes: When to Use Which
- 09Chapter 9: SOLID, Part 1: Single Responsibility & Open/Closed
- 10Chapter 10: SOLID, Part 2: Liskov Substitution, Interface Segregation, Dependency Inversion
- 11Chapter 11: Basic UML for Communicating Design
- 12Chapter 12: Anti-Patterns: God Objects, Tight Coupling, Deep Hierarchies, the Fragile Base Class, Anemic Domain Models
- 13Chapter 13: OOP Idioms Across TypeScript and Python — and Where OOP Is the Wrong Tool
- 14Chapter 14: When OOP Breaks Down: Real-World Failure Modes
- 15Chapter 15: Design Exercise: Parking Lot System
- 16Chapter 16: Design Exercise: Library Management System
- 17Chapter 17: Design Exercise: Elevator Controller
- 18Chapter 18: OOP in Interviews: What a Strong Answer Looks Like
Chapter 15 of 18
Chapter 15: Design Exercise: Parking Lot System
Why this matters
The parking lot is one of the most common OOP design exercises in interviews precisely because it's small enough to finish in 30–45 minutes but touches almost every pillar and principle in this book: encapsulation of state (spot availability), polymorphism (vehicle types, spot types), composition (a lot has floors has spots), and a couple of real trade-off decisions with no single "correct" answer. Working through it once, deliberately, builds the pattern-matching you'll reuse on any similar exercise.
Requirements
A strong candidate clarifies requirements before designing anything. For this exercise:
- The lot has multiple floors, each with multiple spots.
- Spots come in different sizes: motorcycle, compact, large (a large vehicle needs a large spot; a motorcycle can fit any spot, but shouldn't waste a large one if avoidable).
- A vehicle enters, gets assigned a spot if one is available, and is issued a ticket.
- A vehicle exits, pays based on duration, and its spot is freed.
- (Clarifying question a strong candidate asks: "Should payment support multiple methods?" — assume yes, to justify Chapter 8's interface pattern.)
The design
Class diagram (UML from Chapter 11):
+------------------+ +------------------+ +------------------+
| ParkingLot | ◆──── | Floor | ◆──── | Spot |
+------------------+ 1 * +------------------+ 1 * +------------------+
| - floors: Floor[]| | - spots: Spot[] | | - size: SpotSize |
+------------------+ +------------------+ | - occupied: bool |
+------------------+
^
▷ (inheritance/impl)
+---------+---------+
| |
+----------------+ +----------------+
| MotorcycleSpot | | LargeSpot |
+----------------+ +----------------+
+------------------+ +------------------+
| Ticket | ───── | Vehicle | (association)
+------------------+ +------------------+
^ ▷
+------------+------------+
| | |
+-----------+ +-----------+ +-----------+
| Motorcycle| | Compact | | Large |
+-----------+ +-----------+ +-----------+
Spot assignment — polymorphism (Chapter 6) decides fit, not a type-check chain:
enum SpotSize { Motorcycle, Compact, Large }
interface Vehicle {
readonly licensePlate: string;
requiredSpotSize(): SpotSize;
}
class Motorcycle implements Vehicle {
constructor(public licensePlate: string) {}
requiredSpotSize(): SpotSize { return SpotSize.Motorcycle; }
}
class Car implements Vehicle {
constructor(public licensePlate: string) {}
requiredSpotSize(): SpotSize { return SpotSize.Compact; }
}
class Truck implements Vehicle {
constructor(public licensePlate: string) {}
requiredSpotSize(): SpotSize { return SpotSize.Large; }
}
class Spot {
private occupied = false;
constructor(public readonly size: SpotSize) {}
canFit(vehicle: Vehicle): boolean {
return !this.occupied && vehicle.requiredSpotSize() <= this.size; // enum ordered by size
}
park(): void { this.occupied = true; }
vacate(): void { this.occupied = false; }
isOccupied(): boolean { return this.occupied; }
}
Encapsulation (Chapter 3): Ticket protects its own lifecycle invariant — it can't be closed twice, and the fee can't be computed before exit:
class Ticket {
private readonly entryTime: number;
private exitTime: number | null = null;
constructor(public readonly vehicle: Vehicle, public readonly spot: Spot) {
this.entryTime = Date.now();
}
closeOut(): number {
if (this.exitTime !== null) throw new Error("Ticket already closed");
this.exitTime = Date.now();
const hours = Math.ceil((this.exitTime - this.entryTime) / (1000 * 60 * 60));
return hours * 500; // 500 cents/hour, flat rate for this exercise
}
}
Interface + DIP (Chapters 8 & 10) for the payment step, since the requirements call for multiple payment methods:
interface PaymentMethod {
pay(amountCents: number): boolean;
}
class CreditCardPayment implements PaymentMethod {
pay(amountCents: number): boolean { console.log(`Charged card: ${amountCents}`); return true; }
}
class CashPayment implements PaymentMethod {
pay(amountCents: number): boolean { console.log(`Cash received: ${amountCents}`); return true; }
}
class ParkingLot {
constructor(private floors: Floor[]) {}
parkVehicle(vehicle: Vehicle): Ticket | null {
for (const floor of this.floors) {
const spot = floor.findAvailableSpot(vehicle);
if (spot) {
spot.park();
return new Ticket(vehicle, spot);
}
}
return null; // lot full
}
exitVehicle(ticket: Ticket, payment: PaymentMethod): boolean {
const amountDue = ticket.closeOut();
const paid = payment.pay(amountDue);
if (paid) ticket.spot.vacate();
return paid;
}
}
class Floor {
constructor(private spots: Spot[]) {}
findAvailableSpot(vehicle: Vehicle): Spot | null {
// Prefer the smallest spot that fits, to avoid wasting large spots on small vehicles.
const candidates = this.spots
.filter(s => s.canFit(vehicle))
.sort((a, b) => a.size - b.size);
return candidates[0] ?? null;
}
}
from enum import IntEnum
from typing import Protocol
import time
class SpotSize(IntEnum):
MOTORCYCLE = 0
COMPACT = 1
LARGE = 2
class Vehicle(Protocol):
license_plate: str
def required_spot_size(self) -> SpotSize: ...
class Spot:
def __init__(self, size: SpotSize) -> None:
self.size = size
self._occupied = False
def can_fit(self, vehicle: Vehicle) -> bool:
return not self._occupied and vehicle.required_spot_size() <= self.size
def park(self) -> None: self._occupied = True
def vacate(self) -> None: self._occupied = False
class Ticket:
def __init__(self, vehicle: Vehicle, spot: Spot) -> None:
self.vehicle = vehicle
self.spot = spot
self._entry_time = time.time()
self._exit_time: float | None = None
def close_out(self) -> int:
if self._exit_time is not None:
raise ValueError("Ticket already closed")
self._exit_time = time.time()
hours = max(1, int((self._exit_time - self._entry_time) // 3600) + 1)
return hours * 500
Trade-offs a strong candidate would raise
[SENIOR INSIGHT]
- Spot assignment strategy: "nearest available" vs. "smallest fitting spot" vs. "load-balance across floors" are all reasonable; naming the trade-off (utilization efficiency vs. walking distance for the driver) matters more than picking one.
- Concurrency: if two vehicles arrive simultaneously,
findAvailableSpotandpark()need to be atomic together, or two tickets could be issued for the same spot — worth explicitly flagging even if the interviewer doesn't ask (this connects directly to Chapter 14's concurrency failure mode). - Why an interface for
PaymentMethodbut not forSpot:PaymentMethodhas a real, current need for multiple implementations (from the requirements);Spotsubclasses could be replaced with a single class parameterized bySpotSize, avoiding an unnecessary hierarchy (Chapter 14's premature-abstraction lesson applied live, during the interview).
Common mistakes
- Jumping straight to code without clarifying requirements — payment methods, spot-size rules, and multi-floor behavior all change the design meaningfully.
- Building a
MotorcycleSpot/CompactSpot/LargeSpotclass hierarchy when a singleSpotclass parameterized by an enum does the same job with less ceremony — a live example of Chapter 14's premature-abstraction failure mode. - Ignoring concurrency entirely. Even a "just design the classes" exercise benefits from a candidate noting where a race condition could occur.
- Over-engineering the payment step with a full payment-gateway abstraction when the exercise only asked for spot assignment and ticketing — matching design depth to what was actually asked is itself a signal of judgment.
Discussion / interview questions
-
"Walk through your class design for a parking lot, and justify why
PaymentMethodis an interface butSpotisn't a class hierarchy." Strong answer: Cites the real, current need for multiple payment methods (interface justified) versus spot size being adequately modeled by a field on one class (hierarchy not justified) — applying Chapter 14's premature-abstraction lesson explicitly. -
"How would you handle two vehicles arriving at the same time for the last available spot?" Strong answer: Identifies the check-then-act race (find spot, then park) and proposes a synchronization mechanism or atomic reservation step — showing awareness of Chapter 14's concurrency failure mode even in a single-process design discussion.
-
"How would you extend this design to support reserved/VIP spots?" Strong answer: Proposes adding a spot attribute or a separate
ReservedSpotcheck (not necessarily a new class) and reasons about where that logic belongs (Spot.canFit, or a filtering step inFloor) without over-restructuring the existing design. -
"What did you choose not to model, and why?" Strong answer: Explicitly scopes out things like a full pricing-tier engine, multi-lot systems, or a real payment gateway integration, explaining that they weren't part of the stated requirements — showing the same judgment used to avoid premature abstraction.
Chapter summary / cheat sheet
- The parking lot exercise exercises composition (
ParkingLot → Floor → Spot), polymorphism (Vehicletypes deciding their own required spot size), encapsulation (Ticket's lifecycle invariant), and DIP (PaymentMethodinterface). - Clarify requirements first — spot sizing rules and payment method plurality both materially change the design.
- Use a single parameterized
Spotclass rather than a subclass per spot size (Chapter 14) — reserve real hierarchies/interfaces for genuine multi-implementation needs like payment methods. - Flag the check-then-act concurrency risk in spot assignment, even if not asked — it demonstrates senior-level awareness (Chapter 14).