Vasu Patel logoVasu Patel
← Objects First: OOP Fundamentals
All chapters in this track
  1. 01Chapter 1: Why Object Orientation Exists
  2. 02Chapter 2: Classes, Objects, and Instances, Precisely Defined
  3. 03Chapter 3: The Four Pillars I: Encapsulation
  4. 04Chapter 4: The Four Pillars II: Abstraction
  5. 05Chapter 5: The Four Pillars III: Inheritance
  6. 06Chapter 6: The Four Pillars IV: Polymorphism
  7. 07Chapter 7: Composition vs. Inheritance
  8. 08Chapter 8: Interfaces and Abstract Classes: When to Use Which
  9. 09Chapter 9: SOLID, Part 1: Single Responsibility & Open/Closed
  10. 10Chapter 10: SOLID, Part 2: Liskov Substitution, Interface Segregation, Dependency Inversion
  11. 11Chapter 11: Basic UML for Communicating Design
  12. 12Chapter 12: Anti-Patterns: God Objects, Tight Coupling, Deep Hierarchies, the Fragile Base Class, Anemic Domain Models
  13. 13Chapter 13: OOP Idioms Across TypeScript and Python — and Where OOP Is the Wrong Tool
  14. 14Chapter 14: When OOP Breaks Down: Real-World Failure Modes
  15. 15Chapter 15: Design Exercise: Parking Lot System
  16. 16Chapter 16: Design Exercise: Library Management System
  17. 17Chapter 17: Design Exercise: Elevator Controller
  18. 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, findAvailableSpot and park() 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 PaymentMethod but not for Spot: PaymentMethod has a real, current need for multiple implementations (from the requirements); Spot subclasses could be replaced with a single class parameterized by SpotSize, avoiding an unnecessary hierarchy (Chapter 14's premature-abstraction lesson applied live, during the interview).

Common mistakes

  1. Jumping straight to code without clarifying requirements — payment methods, spot-size rules, and multi-floor behavior all change the design meaningfully.
  2. Building a MotorcycleSpot/CompactSpot/LargeSpot class hierarchy when a single Spot class parameterized by an enum does the same job with less ceremony — a live example of Chapter 14's premature-abstraction failure mode.
  3. Ignoring concurrency entirely. Even a "just design the classes" exercise benefits from a candidate noting where a race condition could occur.
  4. 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

  1. "Walk through your class design for a parking lot, and justify why PaymentMethod is an interface but Spot isn'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.

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

  3. "How would you extend this design to support reserved/VIP spots?" Strong answer: Proposes adding a spot attribute or a separate ReservedSpot check (not necessarily a new class) and reasons about where that logic belongs (Spot.canFit, or a filtering step in Floor) without over-restructuring the existing design.

  4. "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 (Vehicle types deciding their own required spot size), encapsulation (Ticket's lifecycle invariant), and DIP (PaymentMethod interface).
  • Clarify requirements first — spot sizing rules and payment method plurality both materially change the design.
  • Use a single parameterized Spot class 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).