Problem 03 · Complete walkthrough•Intermediate•40 min

Parking Lot

Prefer to learn one decision at a time?

Try questions, reveal the reasoning, trace the code, and finish with the simulator.

Start Focus Mode

Design a parking lot that accepts a vehicle, chooses a suitable free spot, issues a ticket, and frees the same spot when the vehicle leaves.

A car entering a two-floor parking garage and following an assigned route to a highlighted compatible free spot
The real problem in one picture: find a spot that is both free and suitable, remember the assignment, and make that spot available again after exit.

1. Requirements (~5 minutes)

The interview begins with a short prompt:

“Design a parking lot.”

That sentence sounds concrete because we have all used a parking lot. But it leaves the rules undecided. Can a motorcycle use a compact spot? Are there several floors? Who chooses the spot? Do we calculate a fee? If we answer those questions silently, we may build a correct solution to the wrong problem.

Start by turning the physical story into actions the program must perform. Do not name classes yet.

Use these four question groups every time

1
Actions

What must a user be able to do?

2
Rules

When does an action succeed, fail, or change state?

3
Errors

Which invalid actions must we reject?

4
Boundaries

What should we deliberately not build?

Ask these questions

AskWhich vehicle and spot types must the first version support?

Interviewer answer

Support motorcycles and cars. Spots are motorcycle, compact, or large. A motorcycle fits any spot; a car fits compact or large.

AskDoes the lot have several floors, and what does nearest mean?

Interviewer answer

Yes. Floor 0 is closest to the entrance, then floor 1, and so on. On one floor, prefer the smallest suitable spot type and then the lowest spot ID.

AskDoes the driver choose a spot, or does the system assign one?

Interviewer answer

The system assigns the nearest compatible free spot when a vehicle enters.

AskWhat identifies an active parking session, and how does exit work?

Interviewer answer

Entry returns a unique ticket containing the vehicle plate, floor, and spot ID. Exit supplies that ticket ID.

AskWhich invalid actions should be rejected?

Interviewer answer

Reject a vehicle that is already parked, an entry with no compatible spot, and an unknown exit ticket.

AskWill the assignment rule change?

Interviewer answer

Implement nearest compatible spot now, but keep the selection rule replaceable so another lot can use a different policy.

AskDo we need payment, reservations, electric charging, several entrances, or persistent storage?

Interviewer answer

No. Keep them as follow-up discussions. Assume calls reach one in-memory process sequentially.

Confirm the specification

Confirmed specification

7 requirements · 6 boundaries

The first version must

  1. 1.Support motorcycles and cars across motorcycle, compact, and large spots on multiple floors.
  2. 2.A motorcycle fits any spot; a car fits only a compact or large spot.
  3. 3.Assign the nearest compatible free spot: lower floor first, then smallest suitable type, then spot ID.
  4. 4.A successful entry occupies exactly one spot and returns a unique ticket containing the plate, floor, and spot ID.
  5. 5.Reject a duplicate vehicle or a vehicle with no compatible free spot without changing lot state.
  6. 6.A valid ticket releases its recorded spot; an unknown ticket changes nothing.
  7. 7.Keep the assignment rule replaceable, with nearest-compatible as the first strategy.
Not building in this version6 boundaries
  • Payments and pricing
  • Advance reservations
  • Electric charging
  • Several entrances acting concurrently
  • Database or distributed consistency
  • Pedestrian navigation and physical gates

2. Entities and relationships (~3 minutes)

Scan the confirmed requirements for candidate nouns: vehicle, parking lot, floor, spot, ticket, assignment rule, entrance gate, payment, and address. A noun from the real world is only a candidate. It becomes a class when it owns changing information or an important rule.

Final model

Types we will create

Class
Owns changing state or coordinates rules.
Record
A Java record for validated, immutable data with value equality.
Interface
Defines behavior that another implementation can replace.
VehicleRecord

Carries the plate and vehicle type together.

ParkingSpotClass

Owns compatibility, availability, occupation, and release.

ParkingFloorClass

Groups and locates spots on one numbered floor.

ParkingLotClass

Coordinates entry, tickets, indexes, and exit.

ParkingTicketRecord

Records the accepted vehicle-to-spot assignment.

SpotAssignmentStrategyInterface

Keeps the candidate ranking policy replaceable.

SpotSelectionRecord

Returns the selected floor and spot together.

ParkingResultRecord

Returns a named status and optional ticket.

Enums

Enums name a small, fixed set of valid choices or outcomes.

VehicleType

Names the supported vehicle categories.

SpotType

Names spot sizes and their compatibility rules.

ParkingStatus

Explains the result of an entry request.

ExitStatus

Explains the result of an exit request.

Fields

Fields are simple values stored inside another type; they do not need independent behavior in this version.

plate

Identifies a vehicle and prevents duplicate entry.

floorNumber

Orders floors during nearest-spot selection.

spotId

Identifies one spot within a floor.

ticketId

Finds the accepted parking session during exit.

See an entry flow through the objects

Swipe to follow the flow →

A vehicle enters ParkingLot, which asks SpotAssignmentStrategy to select a ParkingFloor and ParkingSpot, then receives a ticket or rejection
Follow the orange arrows: ParkingLot coordinates the request, the strategy chooses, and ParkingSpot owns the actual availability change.

Read the arrows from left to right. A Vehicle enters through ParkingLot.park. ParkingLot checks whether that plate already has an active ticket, then asks SpotAssignmentStrategy for a selection. The strategy reads the ordered ParkingFloor groups and their spots. The chosen ParkingSpot performs occupy(vehicle) because it owns the changing availability. Only after that succeeds does ParkingLot return a ParkingTicket. If no selection exists, it returns a rejection and no spot changes.

The flow teaches an important boundary: the strategy may choose a spot, but it does not occupy it or issue tickets. Selection policy and parking-session state are separate jobs.

3. Class design (10–15 minutes)

Bridge from the requirements using the same chain each time:

requirement → responsible object → state it remembers → method that changes or reads that state

Start with ParkingLot.park because that is the main action visible to the caller. Then follow its needs downward into the strategy, floor, and spot.

One class at a time

Design the state owners

Start with the coordinator, then follow the request into the classes that protect changing state. Select a class to see only the fields and behavior needed for its responsibility.

Class 1 of 3

ParkingLot

Coordinates entry and exit while keeping ticket and vehicle indexes consistent.

Requirement behind this class

Park a vehicle once, return a ticket, exit by ticket ID, and leave accepted state unchanged after rejection.

State it remembers

Private information required to perform this class's job.

List<ParkingFloor> floors

Provides the ordered floors available to selection.

SpotAssignmentStrategy strategy

Delegates the replaceable ranking rule.

Map<String, ParkingTicket> activeTicketsById

Finds the accepted session during exit.

Map<String, String> activeTicketIdByPlate

Rejects duplicate vehicle entry.

int nextTicketNumber

Allocates deterministic IDs only for accepted parking.

Behavior it owns

Public use cases and internal rules kept beside that state.

park(vehicle)Public API

Checks duplication, selects and occupies a spot, then records the ticket.

leave(ticketId)Public API

Finds the recorded spot, releases it, and removes both indexes.

getFloors() / activeTicket(...) Public API

Expose safe read-only views of current state.

createTicket(...) / updateIndexes(...) Internal

Record success only after the chosen spot accepts occupation.

Invariant it protects
Every active ticket, plate index, and occupied spot describe the same accepted parking session.
Works with
ParkingFloor, ParkingSpot, SpotAssignmentStrategy, and immutable ticket/result records.
Does not own
Spot compatibility rules or candidate ranking details.
Principle demonstrated
Validation before mutation protects several related indexes as one workflow.

Supporting types

These types carry stable data or define replaceable behavior. They support the state owners without becoming another place to store live workflow state.

Vehicle, SpotSelection, ParkingTicket, ParkingResultRecord

Carry validated request, selection, receipt, and result data.

They cross boundaries as stable values and never become a second route to mutate live parking state.

SpotAssignmentStrategyInterface

Defines read-only selection from floors and a Vehicle.

It may return SpotSelection but cannot occupy a spot or issue a ticket.

NearestAvailableSpotStrategyImplementation

Filters compatible free spots and compares floor, fit, then spot ID.

The implementation contains ranking behavior only; ParkingLot retains the accepted-state workflow.

The explorer keeps the workflow compact: ParkingLot coordinates, ParkingFloor groups, and ParkingSpot protects occupation. With those boundaries clear, we can examine the one algorithm intentionally delegated outside the state owners.

Understand the allocation algorithm

The practical problem is not merely finding an empty spot. It is finding the first compatible spot according to a rule both interviewer and code understand.

Consider a car entering this lot:

  1. Floor 0 has M-01 free and L-01 free.
  2. Floor 1 has C-11 free.
  3. M-01 is rejected because a car cannot fit there.
  4. L-01 is compatible and is on floor 0.
  5. C-11 is a tighter fit, but floor 1 is farther away.
  6. The strategy chooses L-01 because floor number is the first comparison.

The implementation filters unusable spots, creates a SpotSelection containing the floor and spot, and sorts candidates by three keys:

floor number → fit preference → spot ID

If there are S spots, filtering costs O(S) and sorting costs O(S log S). The lesson uses sorting because the rule is easy to read in an interview. A large fixed lot could maintain ordered free-spot sets and make selection faster, but every occupation and release would then need to keep those indexes correct.

Decide whether a pattern helps

Use: Strategy for spot assignment

The exact behavior expected to vary is select(floors, vehicle). ParkingLot depends on the SpotAssignmentStrategy interface, and the current implementation is NearestAvailableSpotStrategy. If another lot later prefers the cheapest floor or reserves large spots, that rule changes in one implementation while duplicate checks, occupation, ticket creation, and exit remain untouched.

Do not force: Factory for vehicles and spots

Construction is simple and visible: each value needs an ID or plate and an enum type. A factory would hide straightforward constructors without resolving a current rule. Introduce one only if creation later needs several steps or configuration.

Do not force: Singleton for ParkingLot

Tests may create several independent lots, and a company may operate several locations. A normal object models that naturally. Global singleton state would make isolation harder and is not required by “one parking lot” in the prompt.

Connect the classes and review the final blueprint

ParkingLot has floors and one assignment Strategy. A ParkingFloor has ParkingSpots. None of these is a specialized form of another, so composition expresses the relationships better than inheritance.

  • Single responsibility: ParkingLot coordinates sessions, Strategy ranks candidates, Floor groups spots, and Spot owns occupation.
  • Encapsulation: callers cannot replace the spot lists or edit an occupant directly.
  • Immutability: vehicles, selections, tickets, and results cross boundaries as stable values.
  • Validation before mutation: duplicate and no-spot failures return before occupation, ticket creation, or index changes.
Sketch UML of Parking Lot records

How to read it: Vehicle, ticket, selection, and result records carry completed decisions without exposing mutable parking state.

Design principle: Immutability makes accepted inputs and returned facts safe to share.

4. Implementation (~10 minutes)

The code below is a complete Java 17 program. Expected user failures return named results. Invalid construction, such as a blank spot ID, throws an exception because that is a programmer mistake rather than a normal parking outcome.

What the code does

It models compatible spots on several floors, selects the nearest suitable free spot, prevents duplicate entry, issues active tickets, and releases the recorded spot on exit.

Why it is structured this way

ParkingLot owns the workflow and ticket indexes, the strategy owns only selection, ParkingFloor owns its spot group, and ParkingSpot owns availability. Each rule stays beside the state it needs.

Follow one request through the code

  1. 1A caller sends ParkingLot.park with an immutable Vehicle.
  2. 2ParkingLot rejects an already-active plate before asking for a spot.
  3. 3NearestAvailableSpotStrategy filters and orders compatible free spots.
  4. 4The selected ParkingSpot validates and records the vehicle.
  5. 5ParkingLot creates a ticket and updates both active-ticket indexes.

Code concepts to understand

How Strategy is used hereParkingLot controls the workflow; the strategy answers only one question: which spot should be tried?

ParkingLot.park calls strategy.select(floors, vehicle). The strategy returns Optional.empty() when no candidate exists, or a SpotSelection containing the chosen floor and spot.

The strategy does not occupy the spot, create the ticket, or update active sessions. Those steps remain in ParkingLot, so replacing the selection rule cannot accidentally replace the rest of the parking workflow.

Why SpotSelection contains both floor and spotExit and ticket creation need the floor that owns the chosen spot.

Returning only ParkingSpot would force ParkingLot to scan every floor again to discover where it came from. SpotSelection is an immutable pair that keeps the result of the search together. It has value equality because it is a Java record.

Validation order protects stateEvery expected rejection happens before a ticket or spot is changed.

park checks duplicate plate, then asks for a selection, then occupies the selected spot, and only then creates ticket indexes. leave finds the active ticket before releasing anything. Therefore an unknown ticket and a full-lot rejection leave all existing sessions unchanged.

Why Optional is useful for a missing selectionNo suitable spot is an expected search result, not a broken program.

SpotAssignmentStrategy.select returns Optional<SpotSelection>. The caller must handle “nothing found” explicitly. Exceptions are reserved for impossible internal states or invalid object construction.

What this code does not guarantee about concurrencyThe current scope processes one request at a time inside one in-memory model.

Two entrances could both select the same free spot before either calls occupy. That is a race condition: the result depends on how the operations interleave. occupy prevents the second mutation inside one sequential call, but the complete select → occupy → ticket operation is not protected as one unit.

If concurrent entrances become a requirement, protect that critical section with a lock in one Java process and add a deterministic two-caller test. Across several application servers, use a shared transaction, unique database constraint, optimistic version, or another shared consistency mechanism. An in-process Java lock cannot protect another server.

Fixed value types

Read these first. They define the vocabulary used by the rest of the model. SpotType.canFit contains the compatibility table, so the same rule is not repeated inside every strategy.

VehicleType.java
The supported kinds of entering vehicles
public enum VehicleType {
    MOTORCYCLE,
    CAR
}
SpotType.java
Compatibility and smallest-fit preference for each spot kind
public enum SpotType {
    MOTORCYCLE,
    COMPACT,
    LARGE;

    public boolean canFit(VehicleType vehicleType) {
        return switch (vehicleType) {
            case MOTORCYCLE -> true;
            case CAR -> this == COMPACT || this == LARGE;
        };
    }

    public int preferenceFor(VehicleType vehicleType) {
        if (!canFit(vehicleType)) return Integer.MAX_VALUE;
        return switch (this) {
            case MOTORCYCLE -> 0;
            case COMPACT -> vehicleType == VehicleType.CAR ? 0 : 1;
            case LARGE -> vehicleType == VehicleType.CAR ? 1 : 2;
        };
    }
}
ParkingStatus.java
Every expected result of an entry request
public enum ParkingStatus {
    PARKED,
    ALREADY_PARKED,
    NO_COMPATIBLE_SPOT
}
ExitStatus.java
Every expected result of an exit request
public enum ExitStatus {
    RELEASED,
    TICKET_NOT_FOUND
}

Immutable request and result values

Vehicle, SpotSelection, ParkingTicket, and ParkingResult are records. Read their compact constructors as boundary checks: a valid object can never contain a blank ID or a contradictory result such as PARKED without a ticket.

Vehicle.java
Stable plate and type for one vehicle
import java.util.Locale;
import java.util.Objects;

public record Vehicle(String licensePlate, VehicleType type) {
    public Vehicle {
        if (licensePlate == null || licensePlate.isBlank()) {
            throw new IllegalArgumentException("license plate is required");
        }
        licensePlate = licensePlate.trim().toUpperCase(Locale.ROOT);
        type = Objects.requireNonNull(type, "vehicle type is required");
    }
}
SpotSelection.java
The floor and spot chosen by an assignment strategy
import java.util.Objects;

public record SpotSelection(ParkingFloor floor, ParkingSpot spot) {
    public SpotSelection {
        floor = Objects.requireNonNull(floor);
        spot = Objects.requireNonNull(spot);
    }
}
ParkingTicket.java
Immutable receipt for one accepted parking session
import java.util.Objects;

public record ParkingTicket(
        String id,
        String licensePlate,
        int floorNumber,
        String spotId) {

    public ParkingTicket {
        if (id == null || id.isBlank()) {
            throw new IllegalArgumentException("ticket id is required");
        }
        if (licensePlate == null || licensePlate.isBlank()) {
            throw new IllegalArgumentException("license plate is required");
        }
        if (floorNumber < 0) {
            throw new IllegalArgumentException("floor number cannot be negative");
        }
        if (spotId == null || spotId.isBlank()) {
            throw new IllegalArgumentException("spot id is required");
        }
    }
}
ParkingResult.java
Entry outcome with a ticket only when parking succeeded
import java.util.Objects;
import java.util.Optional;

public record ParkingResult(ParkingStatus status, ParkingTicket ticket) {
    public ParkingResult {
        status = Objects.requireNonNull(status);
        if (status == ParkingStatus.PARKED && ticket == null) {
            throw new IllegalArgumentException("PARKED requires a ticket");
        }
        if (status != ParkingStatus.PARKED && ticket != null) {
            throw new IllegalArgumentException("Rejected parking cannot have a ticket");
        }
    }

    public static ParkingResult parked(ParkingTicket ticket) {
        return new ParkingResult(ParkingStatus.PARKED, Objects.requireNonNull(ticket));
    }

    public static ParkingResult rejected(ParkingStatus status) {
        if (status == ParkingStatus.PARKED) {
            throw new IllegalArgumentException("Use parked() for success");
        }
        return new ParkingResult(status, null);
    }

    public Optional<ParkingTicket> optionalTicket() {
        return Optional.ofNullable(ticket);
    }
}

State-owning objects

Read ParkingSpot.occupy before ParkingFloor. It shows the most important state rule: check compatibility and availability before storing the vehicle. ParkingFloor then provides a safe group and lookup without exposing a mutable list.

ParkingSpot.java
Owns compatibility and the current occupant of one spot
import java.util.Objects;
import java.util.Optional;

public final class ParkingSpot {
    private final String id;
    private final SpotType type;
    private Vehicle occupiedBy;

    public ParkingSpot(String id, SpotType type) {
        if (id == null || id.isBlank()) {
            throw new IllegalArgumentException("spot id is required");
        }
        this.id = id;
        this.type = Objects.requireNonNull(type);
    }

    public boolean canFit(Vehicle vehicle) {
        return vehicle != null && type.canFit(vehicle.type());
    }

    public boolean occupy(Vehicle vehicle) {
        if (occupiedBy != null || !canFit(vehicle)) return false;
        occupiedBy = vehicle;
        return true;
    }

    public boolean release() {
        if (occupiedBy == null) return false;
        occupiedBy = null;
        return true;
    }

    public boolean isAvailable() {
        return occupiedBy == null;
    }

    public String getId() {
        return id;
    }

    public SpotType getType() {
        return type;
    }

    public Optional<Vehicle> getVehicle() {
        return Optional.ofNullable(occupiedBy);
    }
}
ParkingFloor.java
Owns one numbered group of spots and safe spot lookup
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

public final class ParkingFloor {
    private final int number;
    private final List<ParkingSpot> spots;

    public ParkingFloor(int number, List<ParkingSpot> spots) {
        if (number < 0) {
            throw new IllegalArgumentException("floor number cannot be negative");
        }
        Objects.requireNonNull(spots);
        if (spots.isEmpty()) {
            throw new IllegalArgumentException("a floor needs at least one spot");
        }

        Set<String> ids = new HashSet<>();
        for (ParkingSpot spot : spots) {
            Objects.requireNonNull(spot);
            if (!ids.add(spot.getId())) {
                throw new IllegalArgumentException("duplicate spot id on floor: " + spot.getId());
            }
        }
        this.number = number;
        this.spots = List.copyOf(spots);
    }

    public Optional<ParkingSpot> findSpot(String spotId) {
        return spots.stream()
                .filter(spot -> spot.getId().equals(spotId))
                .findFirst();
    }

    public int availableSpotCount() {
        return (int) spots.stream().filter(ParkingSpot::isAvailable).count();
    }

    public int getNumber() {
        return number;
    }

    public List<ParkingSpot> getSpots() {
        return spots;
    }
}

Assignment policy

The interface is intentionally small. The current strategy reads every spot, removes occupied or incompatible candidates, and applies the exact ordering agreed in Requirements. The final spot-ID comparison is the deterministic tie-breaker.

SpotAssignmentStrategy.java
Replaceable boundary for choosing one spot
import java.util.List;
import java.util.Optional;

public interface SpotAssignmentStrategy {
    Optional<SpotSelection> select(List<ParkingFloor> floors, Vehicle vehicle);
}
NearestAvailableSpotStrategy.java
Chooses by floor, smallest suitable type, then spot ID
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public final class NearestAvailableSpotStrategy implements SpotAssignmentStrategy {
    @Override
    public Optional<SpotSelection> select(List<ParkingFloor> floors, Vehicle vehicle) {
        Objects.requireNonNull(floors);
        Objects.requireNonNull(vehicle);

        Comparator<SpotSelection> nearestFirst = Comparator
                .comparingInt((SpotSelection selection) -> selection.floor().getNumber())
                .thenComparingInt(selection ->
                        selection.spot().getType().preferenceFor(vehicle.type()))
                .thenComparing(selection -> selection.spot().getId());

        return floors.stream()
                .flatMap(floor -> floor.getSpots().stream()
                        .filter(ParkingSpot::isAvailable)
                        .filter(spot -> spot.canFit(vehicle))
                        .map(spot -> new SpotSelection(floor, spot)))
                .min(nearestFirst);
    }
}

Parking workflow and demo

Read ParkingLot.park from top to bottom. Its order mirrors the requirement discussion: reject duplicate, select, occupy, issue ticket, index the session. leave reverses only the state recorded by a known ticket. ParkingLotDemo exercises success and both important rejection paths.

ParkingLot.java
Coordinates entry, tickets, spot occupation, and exit
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public final class ParkingLot {
    private final List<ParkingFloor> floors;
    private final SpotAssignmentStrategy strategy;
    private final Map<String, ParkingTicket> activeTicketsById = new LinkedHashMap<>();
    private final Map<String, String> activeTicketIdByPlate = new HashMap<>();
    private int nextTicketNumber = 1;

    public ParkingLot(List<ParkingFloor> floors, SpotAssignmentStrategy strategy) {
        Objects.requireNonNull(floors);
        if (floors.isEmpty()) {
            throw new IllegalArgumentException("parking lot needs at least one floor");
        }
        validateUniqueFloorAndSpotIds(floors);
        this.floors = List.copyOf(floors);
        this.strategy = Objects.requireNonNull(strategy);
    }

    public ParkingResult park(Vehicle vehicle) {
        Objects.requireNonNull(vehicle);
        if (activeTicketIdByPlate.containsKey(vehicle.licensePlate())) {
            return ParkingResult.rejected(ParkingStatus.ALREADY_PARKED);
        }

        SpotSelection selection = strategy.select(floors, vehicle).orElse(null);
        if (selection == null) {
            return ParkingResult.rejected(ParkingStatus.NO_COMPATIBLE_SPOT);
        }
        if (!selection.spot().occupy(vehicle)) {
            throw new IllegalStateException("strategy returned an unusable spot");
        }

        String ticketId = String.format("T%03d", nextTicketNumber++);
        ParkingTicket ticket = new ParkingTicket(
                ticketId,
                vehicle.licensePlate(),
                selection.floor().getNumber(),
                selection.spot().getId());
        activeTicketsById.put(ticketId, ticket);
        activeTicketIdByPlate.put(vehicle.licensePlate(), ticketId);
        return ParkingResult.parked(ticket);
    }

    public ExitStatus leave(String ticketId) {
        if (ticketId == null || ticketId.isBlank()) {
            return ExitStatus.TICKET_NOT_FOUND;
        }
        ParkingTicket ticket = activeTicketsById.get(ticketId);
        if (ticket == null) return ExitStatus.TICKET_NOT_FOUND;

        ParkingSpot spot = floors.stream()
                .filter(floor -> floor.getNumber() == ticket.floorNumber())
                .findFirst()
                .flatMap(floor -> floor.findSpot(ticket.spotId()))
                .orElseThrow(() -> new IllegalStateException("ticket points to a missing spot"));
        if (!spot.release()) {
            throw new IllegalStateException("ticket points to an empty spot");
        }

        activeTicketsById.remove(ticketId);
        activeTicketIdByPlate.remove(ticket.licensePlate());
        return ExitStatus.RELEASED;
    }

    public List<ParkingTicket> getActiveTickets() {
        return List.copyOf(activeTicketsById.values());
    }

    public List<ParkingFloor> getFloors() {
        return floors;
    }

    private static void validateUniqueFloorAndSpotIds(List<ParkingFloor> floors) {
        Set<Integer> floorNumbers = new HashSet<>();
        Set<String> spotIds = new HashSet<>();
        for (ParkingFloor floor : floors) {
            Objects.requireNonNull(floor);
            if (!floorNumbers.add(floor.getNumber())) {
                throw new IllegalArgumentException("duplicate floor: " + floor.getNumber());
            }
            for (ParkingSpot spot : floor.getSpots()) {
                if (!spotIds.add(spot.getId())) {
                    throw new IllegalArgumentException("duplicate spot id: " + spot.getId());
                }
            }
        }
    }
}
ParkingLotDemo.java
Runs accepted entry, unchanged rejection, exit, and full-lot cases
import java.util.List;

public final class ParkingLotDemo {
    public static void main(String[] args) {
        ParkingFloor ground = new ParkingFloor(0, List.of(
                new ParkingSpot("M-01", SpotType.MOTORCYCLE),
                new ParkingSpot("C-01", SpotType.COMPACT),
                new ParkingSpot("L-01", SpotType.LARGE)));
        ParkingFloor first = new ParkingFloor(1, List.of(
                new ParkingSpot("C-11", SpotType.COMPACT),
                new ParkingSpot("L-11", SpotType.LARGE)));

        ParkingLot lot = new ParkingLot(
                List.of(ground, first),
                new NearestAvailableSpotStrategy());

        Vehicle carOne = new Vehicle("ka-01-ab-1001", VehicleType.CAR);
        ParkingResult firstEntry = lot.park(carOne);
        print(firstEntry);

        ParkingResult duplicate = lot.park(carOne);
        System.out.println("Duplicate: " + duplicate.status()
                + ", active tickets: " + lot.getActiveTickets().size());

        print(lot.park(new Vehicle("KA-01-AB-1002", VehicleType.CAR)));
        print(lot.park(new Vehicle("KA-01-AB-1003", VehicleType.CAR)));
        print(lot.park(new Vehicle("KA-01-AB-1004", VehicleType.CAR)));
        print(lot.park(new Vehicle("KA-01-AB-1005", VehicleType.CAR)));

        String firstTicketId = firstEntry.ticket().id();
        System.out.println("Exit " + firstTicketId + ": " + lot.leave(firstTicketId));
        print(lot.park(new Vehicle("KA-01-AB-1005", VehicleType.CAR)));
        System.out.println("Unknown exit: " + lot.leave("T999"));
    }

    private static void print(ParkingResult result) {
        if (result.status() != ParkingStatus.PARKED) {
            System.out.println("Entry: " + result.status());
            return;
        }
        ParkingTicket ticket = result.ticket();
        System.out.printf("%s -> %s on floor %d, spot %s%n",
                result.status(), ticket.id(), ticket.floorNumber(), ticket.spotId());
    }
}

Verify the design with concrete state changes

  1. 1Initial: five spots are free. Floor 0 has motorcycle, compact, and large spots; floor 1 has compact and large spots.
  2. 2Car 1001 enters. The strategy rejects M-01, chooses floor 0 compact spot C-01, and ParkingLot returns T001.
  3. 3Car 1001 enters again. ParkingLot returns ALREADY_PARKED before selection; C-01 and the one active ticket remain unchanged.
  4. 4Four more cars are attempted. L-01, C-11, and L-11 are occupied; the fifth car receives NO_COMPATIBLE_SPOT while motorcycle spot M-01 remains free.
  5. 5leave(T001) finds C-01 from the ticket, releases it, and removes both ticket indexes.
  6. 6Car 1005 tries again and receives C-01. leave(T999) returns TICKET_NOT_FOUND and changes nothing.

Use the simulator to prove the same rules. Park the same plate twice, exit a ticket, and load the compatibility edge case. In that edge case, free spots exist, but a car is still rejected because neither free spot is compatible.

Try the allocation rule

Parking lot simulator

6 free0 active tickets
Floor 0checked first
M-01MOTORCYCLE

Available

C-01COMPACT

Available

L-01LARGE

Available

Floor 1checked second
M-11MOTORCYCLE

Available

C-11COMPACT

Available

L-11LARGE

Available

Selection order: lower floor → smallest suitable spot type → spot ID.

Entry gate

Strategy currently sees 4 compatible free spots.

Active tickets

No vehicles are parked.

What the objects did

  1. Lot ready. Six spots are free across two floors.

5. Extensions (~5 minutes)

“Now support two entrances at the same time”

The changed requirement affects the select → occupy → create-ticket sequence owned by ParkingLot. Two threads must not both act on the same selection. In one Java process, protect that complete critical section with a lock and demonstrate two callers competing for one spot, where exactly one receives PARKED. A lock around only ParkingSpot.occupy prevents two occupants, but the losing request must retry selection if another compatible spot exists. Across servers, move the guarantee to shared transactional storage; a local lock is not enough.

“Prefer reserved or electric-charging spots for eligible vehicles”

Compatibility currently belongs to SpotType, and ranking belongs to SpotAssignmentStrategy. Add the new vehicle eligibility and spot capability, then update or replace the strategy. Do not place charging rules inside ticket creation.

“Calculate a fee at exit”

The ticket must now remember entry time, and exit needs an exit time. Add a PricingStrategy only when several pricing rules are required, such as hourly, weekend, or vehicle-based pricing. Use an injected clock in tests so fee examples remain deterministic.

“Show available counts on entrance boards”

Each floor already owns its spots, so it can calculate counts by spot type. For a small in-memory lot, calculating on request is simplest. If reads become frequent, add maintained counters beside occupy and release, then test that rejected actions do not change them.

“Let drivers reserve a spot before arrival”

A boolean occupied/free state is no longer enough. Introduce explicit spot states such as AVAILABLE, RESERVED, and OCCUPIED, plus a reservation ID and expiry rule. This is the point where a state-transition design becomes useful; do not add those states to the basic version.