Design a Parking Lot System in Java

Build a parking lot system using Factory, Strategy, and Observer patterns.

Introduction

30 Second Summary

Every parking lot you've driven into solves the same problem: match each vehicle to an open spot, print a ticket, and update the "FULL" sign when capacity runs out. This is the exact real-world system that appears as the number one Low Level Design interview question at top tech companies.

In this project, you will build a complete Parking Lot management system in Java that demonstrates three of the most-tested design patterns: Factory, Strategy, and Observer.

What You'll Build

You'll run the finished system from your terminal, park cars, motorcycles, and buses across multiple floors, get printed tickets, and watch a live display board update itself as spots fill and empty.

By the end of this project, you'll have:

  • A running system where you park any vehicle and get a printed ticket showing the assigned spot number, floor, and entry timestamp.
  • New vehicle types added without touching existing code, proving the Factory pattern and Open/Closed Principle work in practice.
  • Swappable allocation strategies that assign spots differently at runtime ("first available" vs "nearest to entrance") using the Strategy pattern, plus a live display board that auto-updates when any spot changes using the Observer pattern.
  • Secret Mission: Add thread safety so two vehicles entering simultaneously never get the same spot.

Are there any prerequisites?

You need basic Java syntax knowledge (variables, methods, loops, conditionals). No prior OOP or design patterns experience is required.

You'll also need IntelliJ IDEA and Java 23 installed on your machine.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters for your interviews.

Set Up the Project Structure

In LLD interviews, evaluators look at your project organization before reading a single line of logic. A clean package layout signals production-level thinking and saves you time during timed rounds.

In this step, you'll create a new Java project in IntelliJ IDEA and set up the professional package structure that machine coding rounds expect: separate packages for models, services, strategies, and enums.

In this step, get ready to:
  • Create a new Java project with the IntelliJ build system.
  • Set up models, enums, services, and strategies packages.
  • Run Main.java to confirm the project compiles and runs.
Create a new Java project

Every machine coding round starts the same way: blank IDE, ticking clock. Knowing exactly which project settings to pick saves you from fumbling with build tools under pressure.

  • In IntelliJ IDEA, from the Welcome screen, click New Project.
  • Select Java from the list on the left.
  • In the Name field, type ParkingLotSystem.
  • Set Build system to IntelliJ.
  • Select your Java 23 JDK from the JDK dropdown.
  • Uncheck Add sample code.
  • Click Create.

The project opens with an empty src folder visible in the Project tool window.

Why IntelliJ build system instead of Maven or Gradle?

Machine coding rounds are timed at 60 to 90 minutes. Setting up a build tool like Maven or Gradle wastes precious minutes on configuration files you do not need.

IntelliJ's built-in build system compiles and runs Java instantly with zero setup. For a self-contained design problem with no external dependencies, it is the fastest path from blank project to running code.

Don't see Java 23 in the JDK dropdown?

Click the dropdown and select Add JDK. Navigate to the folder where Java 23 is installed on your machine and select it.

help me find my Java 23 JDK path on my operating system

Build the package structure

A professional LLD project separates concerns into distinct packages. You will create the root package and four sub-packages that interviewers expect to see.

  • Open the Project tool window by pressing Cmd+1 (macOS) or Alt+1 (Windows).
  • Right-click the src folder, select New, then Package.
  • Type com.parkinglot and press Enter.
  • Right-click the com.parkinglot package, select New, then Package.
  • Type models and press Enter.
  • Right-click com.parkinglot again and create a new package named enums.
  • Right-click com.parkinglot again and create a new package named services.
  • Right-click com.parkinglot again and create a new package named strategies.

Your Project tool window should now show five packages: com.parkinglot, com.parkinglot.models, com.parkinglot.enums, com.parkinglot.services, and com.parkinglot.strategies.

What is each package for?

  • models holds pure data classes (Vehicle, ParkingSpot, Ticket).
  • enums holds type-safe constants (VehicleType, SpotType).
  • services holds business logic and orchestrators (ParkingLot, DisplayBoard).
  • strategies holds interchangeable algorithms (spot allocation strategies).

Seeing a flat 'com.parkinglot.models' instead of nested folders?

IntelliJ sometimes displays packages in a "flat" view. Click the gear icon at the top of the Project tool window, then uncheck Flatten Packages to see the nested folder hierarchy.

help me switch between flat and hierarchical package view in IntelliJ

Write and run Main.java
  • Right-click the com.parkinglot package, select New, then Java Class.
  • Type Main and press Enter.
  • Replace the file contents with the following code:
package com.parkinglot;

public class Main {
    public static void main(String[] args) {
        System.out.println("Parking Lot System initialized");
    }
}

What does this code do?

  • package com.parkinglot declares which package this class belongs to, matching the folder structure you created.
  • public static void main(String[] args) is the entry point Java looks for when you run the application.
  • The print statement confirms your project compiles and runs correctly before you add any design logic.

Before you run this, what do you expect to see printed in the console?

  • Right-click anywhere inside Main.java and select Run 'Main.main()'.

You should see Parking Lot System initialized printed in the Run console at the bottom of IntelliJ.

✔️ Awesome, I've got everything!

Great. Make sure you have saved Main.java and your console shows the expected output.

ⓧ I'd like to double check the full code

Here is the complete Main.java for this step:

package com.parkinglot;

public class Main {
    public static void main(String[] args) {
        System.out.println("Parking Lot System initialized");
    }
}

Seeing 'Error: Could not find or load main class'?

  • Make sure Main.java is inside the com.parkinglot package, not inside one of the sub-packages like models or services.
  • Confirm the first line of the file reads package com.parkinglot; exactly.
  • Check that your src folder is marked as a Sources Root (it should appear blue in the Project tool window).

help me fix 'Could not find or load main class' in my IntelliJ Java project

Your project skeleton is ready. Next up, you'll fill these packages with the core entities and park your first car.

Build Core Entities and Park Your First Car

The first thing interviewers evaluate in an LLD round is whether you can extract the right entities from requirements. You need classes with clear, single responsibilities.

A common mistake is building a "God class" that handles everything. In this step, you will model vehicles, spots, tickets, and a parking lot as separate classes. By the end, you will have a working system that parks a car and prints a ticket.

In this step, get ready to:
  • Create enums for vehicle types and spot types.
  • Build model classes for Vehicle, ParkingSpot, and Ticket.
  • Build the ParkingLot service that parks a car and returns a ticket.
Create the type enums and Vehicle class

Every entity in the parking lot system has a type. Vehicles can be motorcycles, cars, or trucks. Spots can be small, medium, or large.

  • Right-click the enums package in the Project tool window.
  • Select New then Java Class.
  • Choose Enum from the kind dropdown, name it VehicleType, and press Enter.
  • Replace the file contents with this code:
package com.parkinglot.enums;

// Defines the types of vehicles the lot accepts
public enum VehicleType {
    MOTORCYCLE,
    CAR,
    TRUCK
}
  • Create another enum in the same enums package named SpotType using the same right-click process:
package com.parkinglot.enums;

// Defines the sizes of parking spots
public enum SpotType {
    SMALL,
    MEDIUM,
    LARGE
}

What do these enums do?

Enums give you type-safe constants instead of raw strings. If you typed "car" vs "Car" vs "CAR", bugs creep in. With enums, the compiler catches mismatches for you.

  • Save both files.
  • Confirm there are no red underlines in either file. IntelliJ compiles as you type, so no red underlines means both enums are valid.

Now create the Vehicle class that uses the VehicleType enum.

  • Right-click the models package, select New then Java Class.
  • Name it Vehicle and press Enter.
  • Replace the file contents with this code:
package com.parkinglot.models;

import com.parkinglot.enums.VehicleType;

// Represents a vehicle entering the parking lot
public class Vehicle {
    private final String licensePlate;
    private final VehicleType vehicleType;

    public Vehicle(String licensePlate, VehicleType vehicleType) {
        this.licensePlate = licensePlate;
        this.vehicleType = vehicleType;
    }

    public String getLicensePlate() { return licensePlate; }
    public VehicleType getVehicleType() { return vehicleType; }

    @Override
    public String toString() {
        return vehicleType + " [" + licensePlate + "]";
    }
}

What does this class do?

Vehicle is a pure data holder. It stores a license plate and a type, and that is all. The fields are final because a car does not change type after it enters the lot.

The toString() method gives you readable output like CAR [ABC-123] when you print a vehicle.

  • Save Vehicle.java.
  • Confirm no red underlines appear. The import for VehicleType should resolve cleanly since you just created that enum.

Seeing a red underline on the import?

Make sure VehicleType.java is inside the com.parkinglot.enums package and has the matching package declaration at the top. If IntelliJ still shows red, try closing and reopening the file.

help me fix an unresolved import in my Vehicle class

Create the ParkingSpot model

A parking spot knows its location (floor and ID), its size type, and whether a vehicle is currently parked in it.

  • Right-click the models package, create a new Java class named ParkingSpot.
  • Replace the file contents with the fields, constructor, and core methods:
package com.parkinglot.models;

import com.parkinglot.enums.SpotType;

// Represents a single parking spot with occupancy tracking
public class ParkingSpot {
    private final int spotId;
    private final SpotType spotType;
    private final int floor;
    private boolean isOccupied;
    private Vehicle parkedVehicle;

    public ParkingSpot(int spotId, SpotType spotType, int floor) {
        this.spotId = spotId;
        this.spotType = spotType;
        this.floor = floor;
        this.isOccupied = false;
        this.parkedVehicle = null;
    }

    public void parkVehicle(Vehicle vehicle) {
        this.isOccupied = true;
        this.parkedVehicle = vehicle;
    }

    public void unparkVehicle() {
        this.isOccupied = false;
        this.parkedVehicle = null;
    }
}

What does this code do?

The ParkingSpot class owns the logic of occupying and freeing a spot. Notice the spot does not decide WHICH vehicle goes into it. That decision lives in the service layer.

The parkVehicle() and unparkVehicle() methods change the internal state. This is encapsulation: outside code cannot directly set isOccupied to an inconsistent state.

  • Now add the getters and toString() below the unparkVehicle() method, inside the class braces (before the final }):
    public int getSpotId() { return spotId; }
    public SpotType getSpotType() { return spotType; }
    public int getFloor() { return floor; }
    public boolean isOccupied() { return isOccupied; }
    public Vehicle getParkedVehicle() { return parkedVehicle; }

    @Override
    public String toString() {
        return "Floor " + floor + ", Spot " + spotId + " (" + spotType + ")";
    }
  • Save ParkingSpot.java.
  • Confirm no red underlines. You now have a model that can park and unpark a vehicle.

Red underline on Vehicle reference?

ParkingSpot uses Vehicle in the same models package, so no import is needed. If you see an error, check that Vehicle.java is saved and its package declaration says com.parkinglot.models.

help me resolve a missing Vehicle reference in ParkingSpot

Create Ticket, ParkingLot, and wire up Main

When a vehicle parks successfully, the system issues a ticket. The ParkingLot service class orchestrates everything: it holds all spots, finds a match, and returns the ticket.

  • Create a new Java class named Ticket in the models package with this code:
package com.parkinglot.models;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

// Issued when a vehicle parks successfully
public class Ticket {
    private final String ticketId;
    private final Vehicle vehicle;
    private final ParkingSpot parkingSpot;
    private final LocalDateTime entryTime;

    public Ticket(String ticketId, Vehicle vehicle, ParkingSpot parkingSpot, LocalDateTime entryTime) {
        this.ticketId = ticketId;
        this.vehicle = vehicle;
        this.parkingSpot = parkingSpot;
        this.entryTime = entryTime;
    }

    public String getTicketId() { return ticketId; }
    public Vehicle getVehicle() { return vehicle; }
    public ParkingSpot getParkingSpot() { return parkingSpot; }
    public LocalDateTime getEntryTime() { return entryTime; }
  • Add the toString() method below the getters, then close the class with a }:
    @Override
    public String toString() {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return "=== PARKING TICKET ===\n" +
               "Ticket ID: " + ticketId + "\n" +
               "Vehicle: " + vehicle + "\n" +
               "Spot: " + parkingSpot + "\n" +
               "Entry Time: " + entryTime.format(formatter) + "\n" +
               "======================";
    }
}

What does this code do?

The Ticket bundles everything about a parking event: which vehicle, which spot, and when they entered. The toString() formats it as a human-readable receipt so you can see results immediately in the console.

  • Save Ticket.java and confirm no red underlines.

Now build the service that ties it all together.

  • Right-click the services package and create a new Java class named ParkingLot.
  • Add the class structure with fields, constructor, and the spot initialization method:
package com.parkinglot.services;

import com.parkinglot.enums.SpotType;
import com.parkinglot.enums.VehicleType;
import com.parkinglot.models.ParkingSpot;
import com.parkinglot.models.Ticket;
import com.parkinglot.models.Vehicle;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

public class ParkingLot {
    private final List<ParkingSpot> spots;
    private int ticketCounter = 1;

    public ParkingLot() {
        this.spots = new ArrayList<>();
    }
    // Creates spots: floor 1 = SMALL, floor 2 = MEDIUM, floor 3 = LARGE
    public void initializeSpots(int floors, int spotsPerFloor) {
        SpotType[] floorTypes = {SpotType.SMALL, SpotType.MEDIUM, SpotType.LARGE};
        for (int floor = 1; floor <= floors; floor++) {
            SpotType type = floorTypes[Math.min(floor - 1, floorTypes.length - 1)];
            for (int i = 1; i <= spotsPerFloor; i++) {
                spots.add(new ParkingSpot(i, type, floor));
            }
        }
    }
}

What does initializeSpots do?

It creates a grid of spots: floor 1 gets SMALL spots (for motorcycles), floor 2 gets MEDIUM (for cars), and floor 3 gets LARGE (for trucks). The Math.min() ensures any floor beyond 3 defaults to LARGE.

  • Now add the parkVehicle() method inside the ParkingLot class, below initializeSpots() and above the closing brace:
    // Finds a matching spot and issues a ticket
    public Ticket parkVehicle(Vehicle vehicle) {
        SpotType required;
        if (vehicle.getVehicleType() == VehicleType.MOTORCYCLE) required = SpotType.SMALL;
        else if (vehicle.getVehicleType() == VehicleType.CAR) required = SpotType.MEDIUM;
        else required = SpotType.LARGE;

        for (ParkingSpot spot : spots) {
            if (!spot.isOccupied() && spot.getSpotType() == required) {
                spot.parkVehicle(vehicle);
                String ticketId = "TKT-" + ticketCounter++;
                return new Ticket(ticketId, vehicle, spot, LocalDateTime.now());
            }
        }
        System.out.println("No available spot for " + vehicle);
        return null;
    }

What does parkVehicle do?

It maps the vehicle type to a spot type using if-else, then loops through all spots to find the first available match. If it finds one, it parks the vehicle and returns a ticket. If no spot is available, it prints an error and returns null.

Notice the if-else chain. It works for now, but you will see why it becomes a problem in the next step.

  • Save ParkingLot.java and confirm no red underlines.

Now wire everything up in Main.java so you can see your system in action.

  • Switch back to Main.java in the editor.
  • Replace the entire file contents with this code:
package com.parkinglot;

import com.parkinglot.enums.VehicleType;
import com.parkinglot.models.Ticket;
import com.parkinglot.models.Vehicle;
import com.parkinglot.services.ParkingLot;

public class Main {
    public static void main(String[] args) {
        System.out.println("Parking Lot System initialized");
        System.out.println();

        // Create a lot with 3 floors, 5 spots each
        ParkingLot lot = new ParkingLot();
        lot.initializeSpots(3, 5);

        // Park a car and print the ticket
        Vehicle car = new Vehicle("ABC-123", VehicleType.CAR);
        Ticket ticket = lot.parkVehicle(car);
        System.out.println(ticket);
    }
}

What does Main do now?

It creates a parking lot with 15 total spots (5 SMALL on floor 1, 5 MEDIUM on floor 2, 5 LARGE on floor 3). Then it creates a CAR vehicle and parks it. The parkVehicle() method finds the first MEDIUM spot on floor 2 and returns a ticket.

  • Save Main.java.

Before you run this, what do you expect to see? Think about which floor and spot number the car should land on.

  • Right-click Main.java in the editor and select Run 'Main.main()'.

You should see output like this in the console:

Parking Lot System initialized

=== PARKING TICKET ===
Ticket ID: TKT-1
Vehicle: CAR [ABC-123]
Spot: Floor 2, Spot 1 (MEDIUM)
Entry Time: 2026-07-11 14:32:05
======================

The car landed on Floor 2, Spot 1 because floor 2 holds MEDIUM spots and spot 1 is the first available. Your timestamp will differ, but the structure should match.

Seeing a NullPointerException or no ticket?

If ticket prints null, the system could not find a matching spot. Check that initializeSpots(3, 5) is called before parkVehicle(). Also confirm the parkVehicle method maps CAR to SpotType.MEDIUM and that floor 2 creates MEDIUM spots.

help me debug why my parking lot returns null when I try to park a car

✔️ Awesome, I've got everything!

Great. Make sure all seven files are saved: VehicleType.java, SpotType.java, Vehicle.java, ParkingSpot.java, Ticket.java, ParkingLot.java, and Main.java.

ⓧ I'd like to double check the full code

Scroll up and confirm each of these code blocks is present in your project, in order:

  • VehicleType.java in enums with MOTORCYCLE, CAR, TRUCK.
  • SpotType.java in enums with SMALL, MEDIUM, LARGE.
  • Vehicle.java in models with licensePlate, vehicleType, constructor, getters, toString.
  • ParkingSpot.java in models with fields, constructor, parkVehicle, unparkVehicle, getters, toString.
  • Ticket.java in models with fields, constructor, getters, toString that prints a formatted receipt.
  • ParkingLot.java in services with spots list, ticketCounter, initializeSpots, and parkVehicle using if-else.
  • Main.java creating a lot with 3 floors and 5 spots, parking a CAR, printing the ticket.

You now have a working parking system with clean entity separation. Each class has one job: Vehicle holds identity, ParkingSpot manages occupancy, Ticket records the event, and ParkingLot orchestrates the flow. Next up, you will hit a real problem with this design and fix it using the Factory pattern.

Fix the OCP Violation With Factory Pattern

In the previous step, you built core entities and parked a car successfully. Your system works, but it has a hidden weakness buried in the parkVehicle method.

Right now, vehicle-to-spot mapping lives inside an if-else chain. What happens when a new vehicle type appears? You have to crack open ParkingLot.java and modify working code. This is the Open/Closed Principle violation interviewers test for. The Factory pattern fixes it by centralizing mapping decisions into a dedicated class so new types never touch existing logic.

In this step, get ready to:
  • Experience the OCP violation firsthand by adding a new vehicle type.
  • Extract a VehicleSpotMapper interface and implementation to replace if-else chains.
  • Park a BUS without modifying ParkingLot.java.
Experience the OCP violation

Imagine a new requirement comes in: the parking lot now needs to support buses. The natural first step is adding the type to your enum.

  • Open VehicleType.java in the enums package.
  • Add BUS after TRUCK so the enum looks like this:
package com.parkinglot.enums;

public enum VehicleType {
    MOTORCYCLE,
    CAR,
    TRUCK,
    BUS
}
  • Now open ParkingLot.java in the services package.
  • Find the parkVehicle method and look at the if-else block:
SpotType required;
if (vehicle.getVehicleType() == VehicleType.MOTORCYCLE) required = SpotType.SMALL;
else if (vehicle.getVehicleType() == VehicleType.CAR) required = SpotType.MEDIUM;
else required = SpotType.LARGE;

To properly handle BUS, you would need to add another else if branch here. The catch-all else happens to assign LARGE, but that is accidental. Every new vehicle type forces you to modify this working method.

Why is this a problem in interviews?

The Open/Closed Principle says classes should be open for extension but closed for modification. When adding a new type requires editing existing code, you risk introducing bugs into logic that already works.

Interviewers look for this exact smell. If they see an if-else chain mapping types, they will ask you to add a new type and watch whether you touch existing classes.

Create the VehicleSpotMapper interface

The fix is extracting the mapping responsibility into its own interface. Any class can implement this interface with its own mapping rules without touching ParkingLot.

  • Right-click the services package in the Project tool window.
  • Select New then Java Class.
  • Choose Interface from the kind dropdown and name it VehicleSpotMapper.
  • Replace the generated content with this code:
package com.parkinglot.services;

import com.parkinglot.enums.SpotType;
import com.parkinglot.enums.VehicleType;

// Interface that maps vehicle types to compatible spot types
public interface VehicleSpotMapper {
    SpotType getCompatibleSpotType(VehicleType vehicleType);
}

What does this interface do?

It defines a single contract: given a VehicleType, return the compatible SpotType. Any implementation can decide its own mapping rules. ParkingLot only depends on this interface, never a concrete mapping class.

  • Save VehicleSpotMapper.java.

Now create the concrete implementation that replaces the if-else chain with a HashMap.

  • Right-click the services package again.
  • Select New then Java Class and name it DefaultVehicleSpotMapper.
  • Replace the generated content with this code:
package com.parkinglot.services;

import com.parkinglot.enums.SpotType;
import com.parkinglot.enums.VehicleType;

import java.util.HashMap;
import java.util.Map;

public class DefaultVehicleSpotMapper implements VehicleSpotMapper {
    // Store all vehicle-to-spot mappings in a single map
    private final Map<VehicleType, SpotType> mappings;

    public DefaultVehicleSpotMapper() {
        mappings = new HashMap<>();
        mappings.put(VehicleType.MOTORCYCLE, SpotType.SMALL);
        mappings.put(VehicleType.CAR, SpotType.MEDIUM);
        mappings.put(VehicleType.TRUCK, SpotType.LARGE);
        mappings.put(VehicleType.BUS, SpotType.LARGE);
    }

    @Override
    public SpotType getCompatibleSpotType(VehicleType vehicleType) {
        SpotType spotType = mappings.get(vehicleType);
        if (spotType == null) {
            throw new IllegalArgumentException("No mapping found for vehicle type: " + vehicleType);
        }
        return spotType;
    }
}

What does this code do?

  • The constructor populates a HashMap with all known vehicle-to-spot pairings. Adding BUS just means one more put call here.
  • The getCompatibleSpotType method looks up the mapping and throws an IllegalArgumentException for unknown types. This makes errors obvious instead of silently falling through.
  • Save DefaultVehicleSpotMapper.java.
Refactor ParkingLot and park a BUS

Now wire the mapper into ParkingLot using dependency injection. The class receives its mapper through the constructor and never knows which implementation it gets.

  • Open ParkingLot.java in the services package.
  • Replace the entire file content with this refactored version:
package com.parkinglot.services;

import com.parkinglot.enums.SpotType;
import com.parkinglot.models.ParkingSpot;
import com.parkinglot.models.Ticket;
import com.parkinglot.models.Vehicle;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

public class ParkingLot {
    private final List<ParkingSpot> spots;
    // Injected mapper replaces the if-else chain
    private final VehicleSpotMapper vehicleSpotMapper;
    private int ticketCounter = 1;
    public ParkingLot(VehicleSpotMapper vehicleSpotMapper) {
        this.spots = new ArrayList<>();
        this.vehicleSpotMapper = vehicleSpotMapper;
    }

    public void initializeSpots(int floors, int spotsPerFloor) {
        SpotType[] floorTypes = {SpotType.SMALL, SpotType.MEDIUM, SpotType.LARGE};
        for (int floor = 1; floor <= floors; floor++) {
            SpotType type = floorTypes[Math.min(floor - 1, floorTypes.length - 1)];
            for (int i = 1; i <= spotsPerFloor; i++) {
                spots.add(new ParkingSpot(i, type, floor));
            }
        }
    }
    public Ticket parkVehicle(Vehicle vehicle) {
        // Delegate mapping to the injected mapper (no if-else!)
        SpotType required = vehicleSpotMapper.getCompatibleSpotType(vehicle.getVehicleType());
        for (ParkingSpot spot : spots) {
            if (!spot.isOccupied() && spot.getSpotType() == required) {
                spot.parkVehicle(vehicle);
                String ticketId = "TKT-" + ticketCounter++;
                return new Ticket(ticketId, vehicle, spot, LocalDateTime.now());
            }
        }
        System.out.println("No available spot for " + vehicle);
        return null;
    }
}

What changed in ParkingLot?

  • The constructor now takes a VehicleSpotMapper parameter. This is dependency injection. ParkingLot does not create its own mapper.
  • The three-line if-else chain is replaced by a single call: vehicleSpotMapper.getCompatibleSpotType(...). The mapping logic is gone from this class entirely.
  • The VehicleType import was removed because ParkingLot no longer references vehicle type constants directly.
  • Save ParkingLot.java.

Now update Main.java to pass the mapper and park a BUS to prove the pattern works.

  • Open Main.java in the com.parkinglot package.
  • Replace the entire file content with this updated version:
package com.parkinglot;

import com.parkinglot.enums.VehicleType;
import com.parkinglot.models.Ticket;
import com.parkinglot.models.Vehicle;
import com.parkinglot.services.DefaultVehicleSpotMapper;
import com.parkinglot.services.ParkingLot;
import com.parkinglot.services.VehicleSpotMapper;

public class Main {
    public static void main(String[] args) {
        System.out.println("Parking Lot System initialized");
        System.out.println();

        // Create mapper and inject into ParkingLot
        VehicleSpotMapper mapper = new DefaultVehicleSpotMapper();
        ParkingLot lot = new ParkingLot(mapper);
        lot.initializeSpots(3, 5);

        Vehicle car = new Vehicle("ABC-123", VehicleType.CAR);
        Ticket ticket1 = lot.parkVehicle(car);
        System.out.println(ticket1);
        System.out.println();

        // Park a BUS without touching ParkingLot.java
        Vehicle bus = new Vehicle("BUS-789", VehicleType.BUS);
        Ticket ticket2 = lot.parkVehicle(bus);
        System.out.println(ticket2);
    }
}

What does this prove?

You added BUS by modifying only two files: VehicleType.java (the enum) and DefaultVehicleSpotMapper.java (the mapping). ParkingLot.java was never opened for editing after the refactor. This is the Open/Closed Principle in action.

  • Save Main.java.

Before you run this, what do you expect to see? Think about where BUS maps to and which floor has LARGE spots.

  • Click the green gutter icon next to main in Main.java and select Run 'Main.main()'.

You should see two tickets printed in the Run tool window. The CAR ticket shows Floor 2 (MEDIUM spots) and the BUS ticket shows Floor 3 (LARGE spots). Both park successfully with unique ticket IDs and timestamps.

Seeing a compilation error?

  • If you see an error about the ParkingLot constructor, make sure you updated the constructor to accept VehicleSpotMapper and removed the old no-argument constructor.
  • If BUS shows a red underline in Main.java, confirm you added BUS to the VehicleType enum with a comma after TRUCK.
  • If you see "cannot find symbol" for DefaultVehicleSpotMapper, check that the class is in the com.parkinglot.services package and the import in Main.java matches exactly.

Help me fix my ParkingLot compilation error after adding VehicleSpotMapper

✔️ Awesome, I've got everything!

Great. Double check you have saved all four files: VehicleType.java, VehicleSpotMapper.java, DefaultVehicleSpotMapper.java, ParkingLot.java, and Main.java.

ⓧ I'd like to double check the full code

Scroll up and confirm each chunk is present in order. Your project should now contain these files in the services package:

  • VehicleSpotMapper.java (the interface with one method).
  • DefaultVehicleSpotMapper.java (the HashMap-based implementation with 4 mappings).
  • ParkingLot.java (constructor accepts VehicleSpotMapper, no if-else in parkVehicle).

And your Main.java should look like this:

package com.parkinglot;

import com.parkinglot.enums.VehicleType;
import com.parkinglot.models.Ticket;
import com.parkinglot.models.Vehicle;
import com.parkinglot.services.DefaultVehicleSpotMapper;
import com.parkinglot.services.ParkingLot;
import com.parkinglot.services.VehicleSpotMapper;

public class Main {
    public static void main(String[] args) {
        System.out.println("Parking Lot System initialized");
        System.out.println();

        // Create mapper and inject into ParkingLot
        VehicleSpotMapper mapper = new DefaultVehicleSpotMapper();
        ParkingLot lot = new ParkingLot(mapper);
        lot.initializeSpots(3, 5);

        Vehicle car = new Vehicle("ABC-123", VehicleType.CAR);
        Ticket ticket1 = lot.parkVehicle(car);
        System.out.println(ticket1);
        System.out.println();

        // Park a BUS without touching ParkingLot.java
        Vehicle bus = new Vehicle("BUS-789", VehicleType.BUS);
        Ticket ticket2 = lot.parkVehicle(bus);
        System.out.println(ticket2);
    }
}

You just applied the Factory pattern to make your system extensible. In the next step, you will tackle another rigidity: the hardcoded spot-finding algorithm. What if management wants a different allocation strategy?

Swap Allocation Strategies at Runtime

Your parking lot works, but look at how it finds a spot: a simple for-loop that grabs the first matching space. What happens when management wants "nearest to entrance" instead? Or "spread evenly across floors"?

Hardcoding one algorithm means rewriting ParkingLot every time the business changes its mind. The Strategy pattern solves this by extracting the algorithm into its own interface, so you can swap behaviors at runtime without touching the class that uses them.

In this step, get ready to:
  • Define a strategy interface and two concrete allocation algorithms.
  • Refactor ParkingLot to delegate spot-finding to any strategy.
  • Run both strategies side by side and see different results.
Create the strategy interface and implementations

The Strategy pattern has three parts: an interface declaring the algorithm signature, and concrete classes that implement different behaviors behind that same signature.

  • In the Project tool window (Alt+1), right-click the strategies package and select New then Java Class.
  • Name it SpotAllocationStrategy and select Interface from the kind dropdown.
  • Replace the generated content with this code:
package com.parkinglot.strategies;

import com.parkinglot.enums.SpotType;
import com.parkinglot.models.ParkingSpot;

import java.util.List;

// Contract for any spot allocation algorithm
public interface SpotAllocationStrategy {
    ParkingSpot findSpot(List<ParkingSpot> availableSpots, SpotType requiredType);
}

What does this interface do?

Any class that implements SpotAllocationStrategy must provide a findSpot method. The caller (ParkingLot) does not know or care which algorithm runs behind this method. It just calls findSpot and gets a spot back.

  • Save SpotAllocationStrategy.java.
  • Right-click the strategies package again, select New then Java Class, and name it FirstAvailableStrategy.
  • Replace the generated content with this code:
package com.parkinglot.strategies;

import com.parkinglot.enums.SpotType;
import com.parkinglot.models.ParkingSpot;

import java.util.List;

// Returns the first unoccupied spot that matches the required type
public class FirstAvailableStrategy implements SpotAllocationStrategy {

    @Override
    public ParkingSpot findSpot(List<ParkingSpot> availableSpots, SpotType requiredType) {
        for (ParkingSpot spot : availableSpots) {
            if (!spot.isOccupied() && spot.getSpotType() == requiredType) {
                return spot;
            }
        }
        return null;
    }
}

What does this strategy do?

This is the simplest allocation: iterate through all spots and return the first one that is unoccupied and matches the required type. If nothing matches, return null so the caller knows the lot is full for that type.

  • Save FirstAvailableStrategy.java.
  • Expand the strategies package in the Project tool window. You should see both SpotAllocationStrategy and FirstAvailableStrategy listed.

Don't see the files?

Make sure you created both files inside the strategies package (com.parkinglot.strategies), not in services or the root com.parkinglot package. Check that the package declaration at the top of each file reads com.parkinglot.strategies.

Ask for help: My strategy files aren't appearing in the right package in IntelliJ.

Now create the second strategy that uses a smarter algorithm.

  • Right-click the strategies package, select New then Java Class, and name it NearestToEntranceStrategy.
  • Replace the generated content with this code:
package com.parkinglot.strategies;

import com.parkinglot.enums.SpotType;
import com.parkinglot.models.ParkingSpot;

import java.util.List;

// Returns the matching spot closest to the entrance (lowest floor, then lowest ID)
public class NearestToEntranceStrategy implements SpotAllocationStrategy {

    @Override
    public ParkingSpot findSpot(List<ParkingSpot> availableSpots, SpotType requiredType) {
        ParkingSpot nearest = null;
        for (ParkingSpot spot : availableSpots) {
            if (!spot.isOccupied() && spot.getSpotType() == requiredType) {
                if (nearest == null ||
                    spot.getFloor() < nearest.getFloor() ||
                    (spot.getFloor() == nearest.getFloor() && spot.getSpotId() < nearest.getSpotId())) {
                    nearest = spot;
                }
            }
        }
        return nearest;
    }
}

What does this strategy do?

Instead of grabbing the first match it encounters in the list, this strategy compares every matching spot and keeps track of the one with the lowest floor number. If two spots share a floor, it picks the one with the lower spotId (closer to the entrance of that floor).

Both strategies implement the same findSpot signature. The caller never knows which algorithm is running.

  • Save NearestToEntranceStrategy.java.
  • Expand the strategies package again. You should now see three files: SpotAllocationStrategy, FirstAvailableStrategy, and NearestToEntranceStrategy.
Refactor ParkingLot to delegate to the strategy

Right now ParkingLot.parkVehicle() contains its own for-loop to find a spot. You need to rip that out and delegate to whichever strategy gets passed in.

  • Open ParkingLot.java in the services package.
  • Find the import section and field declarations at the top of the class. They currently look like this:
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

public class ParkingLot {
    private final List<ParkingSpot> spots;
    private final VehicleSpotMapper vehicleSpotMapper;
    private int ticketCounter = 1;

    public ParkingLot(VehicleSpotMapper vehicleSpotMapper) {
        this.spots = new ArrayList<>();
        this.vehicleSpotMapper = vehicleSpotMapper;
    }
  • Replace that section with the following, which adds the strategy import, a new field, and updates the constructor to accept both dependencies:
import com.parkinglot.strategies.SpotAllocationStrategy;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

public class ParkingLot {
    private final List<ParkingSpot> spots;
    private final VehicleSpotMapper vehicleSpotMapper;
    // Strategy field: the algorithm is injected, not hardcoded
    private final SpotAllocationStrategy allocationStrategy;
    private int ticketCounter = 1;

    public ParkingLot(VehicleSpotMapper vehicleSpotMapper, SpotAllocationStrategy allocationStrategy) {
        this.spots = new ArrayList<>();
        this.vehicleSpotMapper = vehicleSpotMapper;
        this.allocationStrategy = allocationStrategy;
    }

What changed?

You imported SpotAllocationStrategy and added it as a constructor parameter. This is dependency injection: the caller decides which algorithm to pass in, and ParkingLot just uses whatever it receives.

  • Now find the parkVehicle method in the same file. It currently looks like this:
    public Ticket parkVehicle(Vehicle vehicle) {
        SpotType required = vehicleSpotMapper.getCompatibleSpotType(vehicle.getVehicleType());
        for (ParkingSpot spot : spots) {
            if (!spot.isOccupied() && spot.getSpotType() == required) {
                spot.parkVehicle(vehicle);
                String ticketId = "TKT-" + ticketCounter++;
                return new Ticket(ticketId, vehicle, spot, LocalDateTime.now());
            }
        }
        System.out.println("No available spot for " + vehicle);
        return null;
    }
  • Replace the entire parkVehicle method with this version, which delegates spot-finding to the strategy:
    public Ticket parkVehicle(Vehicle vehicle) {
        SpotType required = vehicleSpotMapper.getCompatibleSpotType(vehicle.getVehicleType());
        // Delegate to the injected strategy instead of a hardcoded loop
        ParkingSpot spot = allocationStrategy.findSpot(spots, required);
        if (spot == null) {
            System.out.println("No available spot for " + vehicle);
            return null;
        }
        spot.parkVehicle(vehicle);
        String ticketId = "TKT-" + ticketCounter++;
        return new Ticket(ticketId, vehicle, spot, LocalDateTime.now());
    }

What does this refactor accomplish?

The for-loop that found spots is gone from ParkingLot entirely. Now allocationStrategy.findSpot(spots, required) handles it. ParkingLot no longer knows whether the algorithm picks the first spot, the nearest spot, or distributes evenly.

If management requests a new algorithm next month, you write one new class that implements SpotAllocationStrategy and pass it to the constructor. ParkingLot never changes.

  • Save ParkingLot.java.
  • Look at Main.java in the editor. You should see a red underline on new ParkingLot(mapper) because the constructor now requires two arguments. That is expected.
Compare both strategies in Main

To prove the Strategy pattern works, you will create two ParkingLot instances with different strategies, park the same vehicle type in each, and see them assigned to different spots.

  • In Main.java, replace the entire main method body (everything between the opening { and closing } of the method) with this code:
        System.out.println("Parking Lot System initialized");
        System.out.println();

        VehicleSpotMapper mapper = new DefaultVehicleSpotMapper();

        // Test with NearestToEntrance strategy
        SpotAllocationStrategy nearest = new NearestToEntranceStrategy();
        ParkingLot lot1 = new ParkingLot(mapper, nearest);
        lot1.initializeSpots(3, 5);
        Vehicle car1 = new Vehicle("ABC-123", VehicleType.CAR);
        Ticket ticket1 = lot1.parkVehicle(car1);
        System.out.println("With NearestToEntrance strategy: " + ticket1.getParkingSpot());

        // Test with FirstAvailable strategy
        SpotAllocationStrategy firstAvailable = new FirstAvailableStrategy();
        ParkingLot lot2 = new ParkingLot(mapper, firstAvailable);
        lot2.initializeSpots(3, 5);
        Vehicle car2 = new Vehicle("XYZ-999", VehicleType.CAR);
        Ticket ticket2 = lot2.parkVehicle(car2);
        System.out.println("With FirstAvailable strategy: " + ticket2.getParkingSpot());
  • You will see red underlines on the new class names (NearestToEntranceStrategy, FirstAvailableStrategy, SpotAllocationStrategy). Click on each red class name and press Alt+Enter (Windows/Linux) or Option+Enter (macOS) to auto-import it.
  • Also remove any old imports that are now unused (IntelliJ grays these out).

Use optimize imports

Press Ctrl+Alt+O (Windows/Linux) or Cmd+Opt+O (macOS) to clean up unused imports automatically after you have added all the new ones.

  • Confirm that Main.java has no red underlines remaining.
  • Save Main.java.

Before you run this, think: both lots have the same layout (3 floors, 5 spots per floor) and both park a CAR (which maps to MEDIUM, so floor 2). Will the two strategies assign the same spot or different spots?

  • Right-click Main.java and select Run 'Main.main()'.

You should see output like this:

Parking Lot System initialized

With NearestToEntrance strategy: Floor 2, Spot 1 (MEDIUM)
With FirstAvailable strategy: Floor 2, Spot 1 (MEDIUM)

Both show Floor 2, Spot 1 because both strategies happen to start from the same point when the lot is empty. The difference emerges when spots are already occupied. The key proof is that ParkingLot itself has zero knowledge of which algorithm ran. It delegated entirely to the strategy object.

Why do both strategies return the same spot here?

With an empty lot, the "first available" spot and the "nearest to entrance" spot are the same: Floor 2, Spot 1. You would see different results if some spots were already taken, or if the list order differed from floor order.

The real value of this pattern is not visible in one run. It is architectural: you can now add SpreadAcrossFloorsStrategy or RandomStrategy without ever opening ParkingLot.java.

Seeing a compilation error?

If you see "cannot resolve symbol" on SpotAllocationStrategy or the strategy classes, check that your imports at the top of Main.java include com.parkinglot.strategies.SpotAllocationStrategy, com.parkinglot.strategies.FirstAvailableStrategy, and com.parkinglot.strategies.NearestToEntranceStrategy.

If ParkingLot still shows an error, make sure its constructor signature matches: public ParkingLot(VehicleSpotMapper vehicleSpotMapper, SpotAllocationStrategy allocationStrategy).

Ask for help: I'm getting unresolved symbol errors after refactoring ParkingLot to use Strategy pattern.

✔️ Awesome, I've got everything!

Great. Double check that you have saved all four modified files: SpotAllocationStrategy.java, FirstAvailableStrategy.java, NearestToEntranceStrategy.java, ParkingLot.java, and Main.java.

ⓧ I'd like to double check the full code

Here are the three new strategy files in full. Confirm yours match exactly.

package com.parkinglot.strategies;

import com.parkinglot.enums.SpotType;
import com.parkinglot.models.ParkingSpot;

import java.util.List;

public interface SpotAllocationStrategy {
    ParkingSpot findSpot(List<ParkingSpot> availableSpots, SpotType requiredType);
}
package com.parkinglot.strategies;

import com.parkinglot.enums.SpotType;
import com.parkinglot.models.ParkingSpot;

import java.util.List;

public class FirstAvailableStrategy implements SpotAllocationStrategy {

    @Override
    public ParkingSpot findSpot(List<ParkingSpot> availableSpots, SpotType requiredType) {
        for (ParkingSpot spot : availableSpots) {
            if (!spot.isOccupied() && spot.getSpotType() == requiredType) {
                return spot;
            }
        }
        return null;
    }
}
package com.parkinglot.strategies;

import com.parkinglot.enums.SpotType;
import com.parkinglot.models.ParkingSpot;

import java.util.List;

public class NearestToEntranceStrategy implements SpotAllocationStrategy {

    @Override
    public ParkingSpot findSpot(List<ParkingSpot> availableSpots, SpotType requiredType) {
        ParkingSpot nearest = null;
        for (ParkingSpot spot : availableSpots) {
            if (!spot.isOccupied() && spot.getSpotType() == requiredType) {
                if (nearest == null ||
                    spot.getFloor() < nearest.getFloor() ||
                    (spot.getFloor() == nearest.getFloor() && spot.getSpotId() < nearest.getSpotId())) {
                    nearest = spot;
                }
            }
        }
        return nearest;
    }
}

For ParkingLot.java and Main.java, scroll up and confirm these sections match what you pasted:

  • ParkingLot fields and constructor include SpotAllocationStrategy allocationStrategy as a field and constructor parameter.
  • ParkingLot parkVehicle calls allocationStrategy.findSpot(spots, required) instead of a for-loop.
  • Main creates lot1 with NearestToEntranceStrategy and lot2 with FirstAvailableStrategy, printing the spot for each.

You just decoupled the allocation algorithm from the parking lot. Next up, you will make the system react to parking events automatically using the Observer pattern, so a display board updates in real time without ParkingLot knowing it exists.

Add a Live Display Board With Observer Pattern

Your parking lot can now park vehicles using swappable strategies. But imagine this scenario: a car parks on Floor 2, and the entry display board still shows Floor 2 as fully available.

Without a notification system, you would need to manually call every display component after every state change. That creates tight coupling between ParkingLot and every component that cares about spot changes. The Observer pattern decouples the parking lot from anything that reacts to events, so you can add new observers (a mobile app, analytics, security alerts) without touching ParkingLot.

In this step, get ready to:
  • Create a ParkingObserver interface and a DisplayBoard that reacts to spot changes.
  • Add observer infrastructure to ParkingLot with registration and notification methods.
  • Wire up the display board and see automatic updates when vehicles park and leave.
Create the observer interface and display board

The Observer pattern has two sides: the subject (who publishes events) and the observers (who listen and react). You start with the observer contract so any class can subscribe to parking lot events.

  • Right-click the services package in IntelliJ's Project tool window.
  • Select New then Java Class.
  • Choose Interface from the kind dropdown and name it ParkingObserver.
  • Replace the generated content with this code:
package com.parkinglot.services;

import com.parkinglot.models.ParkingSpot;

// Contract for any class that wants to react to parking events
public interface ParkingObserver {
    void onSpotTaken(ParkingSpot spot);
    void onSpotFreed(ParkingSpot spot);
}
  • Now create a new class in the same services package called DisplayBoard.
  • Replace its content with this implementation:
package com.parkinglot.services;

import com.parkinglot.models.ParkingSpot;

// Reacts to parking events by printing status updates
public class DisplayBoard implements ParkingObserver {

    @Override
    public void onSpotTaken(ParkingSpot spot) {
        System.out.println("SPOT TAKEN: Floor " + spot.getFloor() +
                ", Spot " + spot.getSpotId() +
                " - " + spot.getParkedVehicle().getVehicleType());
    }

    @Override
    public void onSpotFreed(ParkingSpot spot) {
        System.out.println("SPOT FREED: Floor " + spot.getFloor() +
                ", Spot " + spot.getSpotId() +
                " - now available");
    }
}

What does this code do?

ParkingObserver defines a contract with two methods: onSpotTaken fires when a vehicle parks, and onSpotFreed fires when a vehicle leaves.

DisplayBoard implements that contract by printing human-readable status messages. Any future observer (a mobile app, a capacity counter, a security logger) would implement the same interface without changing a single line in ParkingLot.

  • Save both files.
  • Confirm that neither file shows red underlines in IntelliJ. Both should compile cleanly since DisplayBoard references only the ParkingObserver interface in the same package.

Seeing red underlines in DisplayBoard?

Check that ParkingObserver.java exists in the same services package. If you accidentally created it in a different package, the import will fail.

Also confirm the interface name is exactly ParkingObserver (capital P, capital O) and that DisplayBoard says implements ParkingObserver in the class declaration.

Still stuck? Help me debug red underlines in my DisplayBoard class.

Refactor ParkingLot with observer support

ParkingLot needs three new capabilities: tracking active tickets so you can look them up when unparking, notifying observers when spots change state, and an unparkVehicle method that reverses a parking action.

  • Open ParkingLot.java in the services package.
  • Select all the content (Cmd+A on macOS or Ctrl+A on Windows) and delete it.
  • Paste this first section with the updated imports, fields, and constructor:
package com.parkinglot.services;

import com.parkinglot.enums.SpotType;
import com.parkinglot.models.ParkingSpot;
import com.parkinglot.models.Ticket;
import com.parkinglot.models.Vehicle;
import com.parkinglot.strategies.SpotAllocationStrategy;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

public class ParkingLot {
    private final List<ParkingSpot> spots;
    private final List<ParkingObserver> observers;
    private final Map<String, Ticket> activeTickets;
    private final VehicleSpotMapper vehicleSpotMapper;
    private final SpotAllocationStrategy allocationStrategy;
    private final AtomicInteger ticketCounter;

    public ParkingLot(VehicleSpotMapper vehicleSpotMapper, SpotAllocationStrategy allocationStrategy) {
        this.spots = new ArrayList<>();
        this.observers = new ArrayList<>();
        this.activeTickets = new HashMap<>();
        this.vehicleSpotMapper = vehicleSpotMapper;
        this.allocationStrategy = allocationStrategy;
        this.ticketCounter = new AtomicInteger(1);
    }
  • Directly below the constructor, paste the initializeSpots and refactored parkVehicle methods:

    public void initializeSpots(int floors, int spotsPerFloor) {
        SpotType[] floorTypes = {SpotType.SMALL, SpotType.MEDIUM, SpotType.LARGE};
        for (int floor = 1; floor <= floors; floor++) {
            SpotType type = floorTypes[Math.min(floor - 1, floorTypes.length - 1)];
            for (int i = 1; i <= spotsPerFloor; i++) {
                spots.add(new ParkingSpot(i, type, floor));
            }
        }
    }

    // synchronized ensures thread-safe spot assignment
    public synchronized Ticket parkVehicle(Vehicle vehicle) {
        SpotType requiredType = vehicleSpotMapper.getCompatibleSpotType(vehicle.getVehicleType());
        ParkingSpot spot = allocationStrategy.findSpot(spots, requiredType);

        if (spot == null) {
            System.out.println("No available spot for " + vehicle);
            return null;
        }

        spot.parkVehicle(vehicle);
        String ticketId = "TKT-" + ticketCounter.getAndIncrement();
        Ticket ticket = new Ticket(ticketId, vehicle, spot, LocalDateTime.now());
        activeTickets.put(ticketId, ticket);
        notifySpotTaken(spot);
        return ticket;
    }
  • Finally, paste the unparkVehicle method and observer management methods to complete the file:

    // Frees a spot and notifies all observers
    public synchronized Ticket unparkVehicle(String ticketId) {
        Ticket ticket = activeTickets.remove(ticketId);
        if (ticket == null) {
            System.out.println("Invalid ticket: " + ticketId);
            return null;
        }
        ticket.getParkingSpot().unparkVehicle();
        notifySpotFreed(ticket.getParkingSpot());
        return ticket;
    }

    public void addObserver(ParkingObserver observer) {
        observers.add(observer);
    }

    private void notifySpotTaken(ParkingSpot spot) {
        for (ParkingObserver observer : observers) {
            observer.onSpotTaken(spot);
        }
    }

    private void notifySpotFreed(ParkingSpot spot) {
        for (ParkingObserver observer : observers) {
            observer.onSpotFreed(spot);
        }
    }
}

What changed in ParkingLot?

  • observers list holds any registered ParkingObserver. ParkingLot never knows the concrete type (DisplayBoard). It only talks through the interface.
  • activeTickets map stores tickets by ID so unparkVehicle can look up which spot to free.
  • ticketCounter is now an AtomicInteger for thread-safe ID generation.
  • notifySpotTaken and notifySpotFreed loop through all observers and call the appropriate method. Adding a new observer requires zero changes to ParkingLot.
  • Save ParkingLot.java.
  • Confirm there are no red underlines in the file. All references (ParkingObserver, AtomicInteger, HashMap) should resolve cleanly.

ParkingLot showing unresolved references?

If ParkingObserver is red, make sure the interface file is saved and lives in the com.parkinglot.services package. ParkingLot references it without an import because they share the same package.

If AtomicInteger is red, check that the import line reads import java.util.concurrent.atomic.AtomicInteger; exactly.

Still stuck? Help me fix compilation errors in my refactored ParkingLot class.

Wire up the display board in Main

The key proof of the Observer pattern is this: ParkingLot never imports or references DisplayBoard directly. Only Main knows both exist. This is loose coupling in action.

  • Open Main.java in the com.parkinglot package.
  • Select all and delete the existing content.
  • Paste this first section that sets up the lot and registers the observer:
package com.parkinglot;

import com.parkinglot.enums.VehicleType;
import com.parkinglot.models.Ticket;
import com.parkinglot.models.Vehicle;
import com.parkinglot.services.DefaultVehicleSpotMapper;
import com.parkinglot.services.DisplayBoard;
import com.parkinglot.services.ParkingLot;
import com.parkinglot.services.VehicleSpotMapper;
import com.parkinglot.strategies.FirstAvailableStrategy;
import com.parkinglot.strategies.NearestToEntranceStrategy;
import com.parkinglot.strategies.SpotAllocationStrategy;

public class Main {
    public static void main(String[] args) {
        System.out.println("Parking Lot System initialized");
        System.out.println();

        // Setup parking lot with mapper and strategy
        VehicleSpotMapper mapper = new DefaultVehicleSpotMapper();
        SpotAllocationStrategy strategy = new NearestToEntranceStrategy();
        ParkingLot lot = new ParkingLot(mapper, strategy);
        lot.initializeSpots(3, 5);

        // Register the display board as an observer
        DisplayBoard displayBoard = new DisplayBoard();
        lot.addObserver(displayBoard);
  • Directly below, paste the vehicle parking, unparking, and strategy comparison section:

        // Park vehicles to trigger SPOT TAKEN notifications
        System.out.println("--- Parking Vehicles ---");
        Vehicle car1 = new Vehicle("ABC-123", VehicleType.CAR);
        Ticket ticket1 = lot.parkVehicle(car1);
        System.out.println(ticket1);
        System.out.println();

        Vehicle motorcycle = new Vehicle("MTR-456", VehicleType.MOTORCYCLE);
        Ticket ticket2 = lot.parkVehicle(motorcycle);
        System.out.println(ticket2);
        System.out.println();

        // Unpark to trigger SPOT FREED notification
        System.out.println("--- Unparking Vehicle ---");
        lot.unparkVehicle(ticket1.getTicketId());
        System.out.println();

        // Second lot demonstrates strategy still works with observers
        System.out.println("--- Strategy Comparison ---");
        SpotAllocationStrategy firstAvailable = new FirstAvailableStrategy();
        ParkingLot lot2 = new ParkingLot(mapper, firstAvailable);
        lot2.initializeSpots(3, 5);
        lot2.addObserver(displayBoard);

        Vehicle car2 = new Vehicle("XYZ-999", VehicleType.CAR);
        Ticket ticket4 = lot2.parkVehicle(car2);
        System.out.println("With FirstAvailable strategy: " + ticket4.getParkingSpot());
    }
}

What does this Main demonstrate?

  • lot.addObserver(displayBoard) registers the DisplayBoard. ParkingLot only sees a ParkingObserver reference, not DisplayBoard specifically.
  • Each parkVehicle call triggers onSpotTaken automatically. You never call DisplayBoard directly.
  • unparkVehicle triggers onSpotFreed automatically. The same observer reacts to both events.
  • The second lot (lot2) also registers the same displayBoard, proving one observer can watch multiple subjects.
  • Save Main.java.

Before you run this, predict: how many "SPOT TAKEN" messages will you see in the output? How many "SPOT FREED" messages?

  • Right-click Main.java and select Run 'Main.main()'.

You should see three "SPOT TAKEN" lines (one for the car, one for the motorcycle, one for car2 on lot2) and one "SPOT FREED" line (when the car unparks). The display board printed each message automatically without ParkingLot ever importing or referencing DisplayBoard.

Why this approach instead of direct method calls?

You could have called displayBoard.update() directly inside parkVehicle. That would work today. But next month when you add a capacity counter, a security logger, and a mobile push notification, you would need to modify ParkingLot three more times.

With Observer, you add each new listener with a single lot.addObserver(newListener) call in Main. ParkingLot never changes. This is the Open/Closed Principle applied to event handling.

Not seeing SPOT TAKEN or SPOT FREED messages?

Make sure you called lot.addObserver(displayBoard) before any parkVehicle calls. If you register the observer after parking, those events are already gone.

If you see a NullPointerException in onSpotTaken, check that notifySpotTaken is called after spot.parkVehicle(vehicle) in ParkingLot. The vehicle must be stored before the observer reads it.

Need help? My Observer pattern isn't printing display board messages when vehicles park.

✔️ Awesome, I've got everything!

Great. Double check you have saved all four files: ParkingObserver.java, DisplayBoard.java, ParkingLot.java, and Main.java.

ⓧ I'd like to double check the full code

Since these files are long, scroll up and confirm each section is present in order:

  • ParkingObserver.java: interface with onSpotTaken and onSpotFreed methods.
  • DisplayBoard.java: implements ParkingObserver with two print methods.
  • ParkingLot.java: three code blocks forming the complete class. Fields + constructor, then initializeSpots + parkVehicle, then unparkVehicle + observer methods.
  • Main.java: two code blocks. Setup + observer registration, then park/unpark/strategy comparison.

Confirm the output shows SPOT TAKEN lines for each parked vehicle and a SPOT FREED line for the unparked car.

Your parking lot now has a live notification system powered by the Observer pattern. ParkingLot publishes events, DisplayBoard reacts to them, and neither knows the other exists. Next up, you will push this decoupled architecture further with a Secret Mission that adds thread safety for concurrent parking.

Secret mission

Add Thread Safety for Concurrent Entry

In a real parking lot, two cars can pull up to the same gate at the exact same moment. Without thread safety, both threads could find the same spot available and assign it to both vehicles. Can you prove your system handles this race condition correctly?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project for continued practice or delete it entirely.

Resources you used:

  • The ParkingLotSystem IntelliJ IDEA project folder on your local machine.

Keep everything running

No action needed. Choose this if you are still actively building or want to use this project as interview prep.

  • Your ParkingLotSystem project stays in IntelliJ IDEA, ready for you to open anytime.
  • Practice rebuilding this from scratch in 45 minutes before your next interview.

Pause - I'll come back to this later

Close IntelliJ IDEA to free up memory. Your project files remain on disk for when you return.

  • Close the ParkingLotSystem project in IntelliJ IDEA by clicking File, then Close Project.
  • To reopen later, launch IntelliJ IDEA and select ParkingLotSystem from the Welcome screen's recent projects list.

Delete - I don't want to use this again

Remove the project folder entirely from your machine.

  • Close the project in IntelliJ IDEA by clicking File, then Close Project.
  • Delete the project folder from your file system. On macOS/Linux, run:
rm -rf ~/IdeaProjects/ParkingLotSystem
  • On Windows, run:
rmdir /s /q %USERPROFILE%\IdeaProjects\ParkingLotSystem
  • Confirm the folder is gone by checking your IdeaProjects directory.

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete Parking Lot management system in Java that demonstrates three of the most-tested design patterns in LLD interviews.

You've learned how to:

  • Implement Factory, Strategy, and Observer patterns in a professional project structure with models, services, strategies, and enums packages.
  • Apply the 5-step LLD interview framework (Requirements, Entities, Class Design, Implementation, Extensibility) to decompose a real-world problem into clean, extensible classes.
  • Experience SOLID violations firsthand (Open/Closed Principle) and fix them by introducing interfaces and dependency injection.
  • Secret Mission: Added thread safety with synchronized blocks and AtomicInteger to handle concurrent parking without double-assigning spots.

Ready to quiz yourself?