Low Level Design of a Parking Lot

See the problem statement here : https://codezym.com/question/1

Design a parking lot is one of the famous object-oriented design questions.

Use cases include :

  • park or remove vehicle,

  • search a vehicle where it is parked and

  • display number of free spots on each floor for each vehicle type.

We have a parking lot with multiple floors and each floor will have spots arranged in rows and columns. Each spot will be used to park a particular vehicle type like 2 wheeler or 4 wheeler.

The data which changes is parking spot, its state will change as we park or remove a vehicle. So our solution will be built around ParkingSpot.

class ParkingSpot {
 String spotId;
 int vehicleType;
 String vehicleNumber, 
 String ticketId;
}

As we park or remove a vehicle from a parking spot vehicleNumber and ticketId changes.

Storing Data :

To park a vehicle, first of all we need to find a free spot for the vehicle.

Lets start with a simple data structure: ParkingSpot spots[floors][rows][columns]

  • Now above 3-d array would have sufficed if our park method had floor, row and column already selected i.e. park(int vehicleType, int floor, int row, int column)

  • But manually choosing floor, row and column each time is inefficient. Ideally, we want system to suggest us a parking spot based on just the vehicleType i.e. park(int vehicleType).

We want both of the above things.

One way to do it is to keep a list of free spots for each floor for each vehicleType. i.e. HashMap<String, ParkingSpotList> freeSpots;

keys in the above map are of the form floor+"-"+vehicleType e.g. key "5-2" will keep list of all free parking spots for vehicle type 2 on floor 5.

our ParkingSpotList is nothing but a doubly linked list of ParkingSpot

  • Using above data structure brings down the worst case complexity of finding a free spot from O(floors*row*columns) down to O(floors),

  • Instead of a hashmap we could have simply kept two lists for free spots, one for two wheeler and other for 4 wheeler vehicle. But that would have given us a maximum parallelism of 2 i.e. number of vehicle types.

  • using the map gives us parallelism of floors * number of vehicle types.

  • The extra space used by the hashmap is worth it. As It increases the number of threads which can park or remove vehicles concurrently to floors * number of vehicle types.

Now our ParkingSpot class has to introduce two new variables ParkingSpot previous, next;

class ParkingSpot {
 ParkingSpot previous, next;

 String spotId;
 int vehicleType;
 String vehicleNumber, ticketId;
}

Now apart from building the doubly linked list these two pointers also help us to remove a parking spot at random from the list in O(1) and it can also support park(int vehicleType, int floor, int row, int column) efficiently in O(1).

Each time we access ParkingSpotList, we need to synchronize it.

ParkingSpotList spotsList = freeSpots.get("5-2");
synchronized (spotsList) {
    if (spotsList.size() == 0) continue;
    spot = spotsList.removeRandom();
 }

ParkingSpotList and ParkingSpot will be the backbone of our solution.

our park method will be below code:

for(int floor=startFloor;floor<startFloor+floors;floor++){
            // step 1 : find a floor to park on
            int currentFloor = floor%floors;
            ParkingSpotList spotsList = freeSpots.get(getSpotListKey(currentFloor, vehicleType));
            if(spotsList==null) continue;
            ParkingSpot spot = null;
            // step 2 : synchronize the list and try to remove a free spot
            synchronized (spotsList) {
                if (spotsList.size() == 0) continue;
                spot = spotsList.removeRandom();
            }
            if(spot==null) continue;
            // cache the parking details and return
            return park(spot, vehicleNumber, ticketId);
 }

when removing the vehicle, we again synchronize the list and put the spot back

ParkingSpotList parkingSpotsList = freeSpots.get(getSpotListKey(d[0], spot.getVehicleType()));
        if(parkingSpotsList==null) return 404;
        spot.setVehicleNumber("");
        spot.setTicketId("");
        synchronized (parkingSpotsList){
            parkingSpotsList.add(spot);
 }

We have one more use case:

searchVehicle(String spotId, String vehicleNumber, String ticketId)

  • we can use ConcurrentHashMap<String, ParkingResult> cache, its keys will be vehicleNumber and ticketId. So basically we are caching each key.

  • And we use a ConcurrentHashMap so that we don't get a ConcurrentModificationException when multiple threads try to add parking details to above cache map.

Disclaimer : Below solution is just one of the ways to solve our problem statement, there can be better or different ways to do it. Now jump into the comments section and let's have a discussion.

Whole code :

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class Solution implements Q001ParkingLotInterface {
    private ParkingSpot spots[][][];
    private HashMap<String, ParkingSpotList> freeSpots;
    private BookingData bookingData;
    private int floors, rows, columns;
    private HashSet<Integer> vehicleTypes;
    private Random random;
    Helper helper;
    public Solution(){}

    // use helper objects methods helper.print() and helper.println()
    // for logging normal System.out.println() logs won't appear
    //
    // parking[2][8][15] = parking spot at 3rd floor ,
    //  9th row and 16th column, its spotId will be: "2-8-15"
    //
    // parking[floor][row][col] = "2-1" or "4-1" means an active parking spot
    // or 2-wheeler and 4-wheeler vehicle respectively
    // parking[floor][row][col] = "2-0" or "4-0" means and inactive 
    // parking spot i.e. where a vehicle can't be parked
    public void init(Helper helper, String[][][] parking) {
        floors = parking.length;
        rows = parking[0].length;
        columns = parking[0][0].length;
        spots = new ParkingSpot[floors][rows][columns];
        vehicleTypes = new HashSet<Integer>();
        vehicleTypes.add(2);
        vehicleTypes.add(4);
        freeSpots = new HashMap<String, ParkingSpotList>();
        bookingData = new BookingData();
        random = new Random();
        this.helper=helper;

        // initializing floors map
        for (int floor = 0; floor < floors; floor++){
            for (int vehicleType : vehicleTypes) {
                String listKey = getSpotListKey(floor, vehicleType);
                freeSpots.put(listKey,  new ParkingSpotList());
            }
        }

        // initializing spots
        for(int floor=0;floor<floors;floor++)
            for(int row=0;row<rows;row++)
                for(int column=0;column<columns;column++){
                    String []ss=parking[floor][row][column].split("-");
                    int vehicleType = Integer.parseInt(ss[0]);
                    boolean isActive= Integer.parseInt(ss[1])==1;
                    String spotId = helper.getSpotId(floor, row,column);
                    ParkingSpot spot = new ParkingSpot(spotId, vehicleType, isActive);
                    spots[floor][row][column]=spot;
                    if(isActive)freeSpots.get(getSpotListKey(floor, vehicleType)).add(spot);
                }
    }

    /**
     * choose a spot of your own choice
     * 400 : for bad request like invalid input parameters vehicle type 
     * not found or both of vehicleNumber and ticketId are blank.
     */
    public ParkingResult park(int vehicleType, String vehicleNumber, String ticketId){
        if(!vehicleTypes.contains(vehicleType)|| (vehicleNumber.isBlank() && ticketId.isBlank()))
            return new ParkingResult(400,"","","");
        int startFloor = random.nextInt(floors);
        for(int floor=startFloor;floor<startFloor+floors;floor++){
            int currentFloor = floor%floors;
            ParkingSpotList spotsList = freeSpots.get(getSpotListKey(currentFloor, vehicleType));
            if(spotsList==null) continue;
            //return new ParkingResult(404,"","","");
            ParkingSpot spot = null;
            synchronized (spotsList) {
                if (spotsList.size() == 0) continue;
                spot = spotsList.removeRandom();
            }
            if(spot==null) continue;
            return park(spot, vehicleNumber, ticketId);
        }

        return new ParkingResult(404,"","","");
    }

    /**
     * this method does the actual parking once an spot has been allocated
     * should be called by all park methods.
     */
    private ParkingResult park(ParkingSpot spot, String vehicleNumber, String ticketId){
        if(vehicleNumber==null) vehicleNumber="";
        if(ticketId==null)ticketId="";
        ParkingResult result = new ParkingResult(201,spot.getSpotId(), vehicleNumber, ticketId);
        spot.setTicketId(ticketId);
        spot.setVehicleNumber(vehicleNumber);
        bookingData.add(result);
        return result;
    }

    /**
     * This method un-parks a vehicle
     * return status types based on ParkingResult
     * - 201 success, 404 : vehicle not found or any other error,
     * exactly one of spotId, vehicleNumber or ticketId will be non empty
     */
    public int removeVehicle(String spotId, String vehicleNumber, String ticketId){
        ParkingResult parked = searchVehicle(spotId, vehicleNumber, ticketId);
        if(parked==null) return 404;
        Integer []d = helper.getSpotLocation(spotId);
        if(!isValidLocation(d)) return 404;
        ParkingSpot spot = spots[d[0]][d[1]][d[2]];
        if(!spot.getTicketId().equals(parked.getTicketId())
                || !spot.getVehicleNumber().equals(parked.getVehicleNumber()))
            return 404;
        ParkingSpotList parkingSpotsList = freeSpots.get(getSpotListKey(d[0], spot.getVehicleType()));
        if(parkingSpotsList==null) return 404;
        spot.setVehicleNumber("");
        spot.setTicketId("");
        synchronized (parkingSpotsList){
            parkingSpotsList.add(spot);
        }
        return 201;
    }

    /** 200 success, 404 not found
     * exactly one of spotId, vehicleNumber or ticketId will be non empty
     */
    public ParkingResult searchVehicle(String spotId, String vehicleNumber, String ticketId){
        if(spotId!=null && !spotId.isBlank()){
            Integer d[]=helper.getSpotLocation(spotId);
            if(!isValidLocation(d))
                return new ParkingResult(404,"","","");
            ParkingSpot spot = spots[d[0]][d[1]][d[2]];
            return bookingData.search(spot.getVehicleNumber(), spot.getTicketId());
        }
        return bookingData.search(vehicleNumber, ticketId);
    }

    private  boolean isValidLocation(Integer index[]){
        return !(index==null || index.length<3 || index[0]<0||index[1]<0||
                index[2]<0 || index[0]>=floors||index[1]>=rows||index[2]>=columns);
    }

    public int getFreeSpotsCount(int floor, int vehicleType){
        ParkingSpotList spotsList = freeSpots.get(getSpotListKey(floor, vehicleType));
        if(spotsList==null) return 0;
        synchronized (spotsList){
            return spotsList.size();
        }
    }


    private String getSpotListKey(int floor, int vehicleType){
        return ""+floor+"-"+vehicleType;
    }

}


/**
 * It's essentially a doubly linked list;
 * This class methods are not thread safe
 */
class ParkingSpotList{
    private ParkingSpot head, tail;
    private AtomicInteger size;

    ParkingSpotList(){
        size=new AtomicInteger(0);
        head=null;
        tail=null;
    }

    /**
     * returns null or the parking spot
     */
    ParkingSpot removeRandom(){
        // System.out.println("items left "+size.get());
        if(size.get()==0) return null; // head==null
        if(size.get() ==1){ // head.getNext()==null
            ParkingSpot temp = head;
            head=null;
            tail=null;
            size.set(0);
            return temp;
        }
        ParkingSpot spot = head;
        int removed =remove(head);
        if(removed>=400) return null;
        return spot;
    }

    /**
     * adds element at the end of list
     */
    void add(ParkingSpot spot){
        spot.setNext(null);
        spot.setPrev(null);
        // if it is first element
        if(head==null){
            head=spot;
            tail=spot;
            size.set(1);
            return;
        }
        tail.setNext(spot);
        spot.setPrev(tail);
        tail=spot;
        size.incrementAndGet();
    }

    /**
     * returns 201 for success,  400 if list is empty, 404 if spot is already assigned
     */
    int remove(ParkingSpot spot){
        // System.out.println("items left "+size.get()+"  head "+head);
        if(size.get()==0) return 400;  // head==null ||
        synchronized (spot) {
            if(!spot.getTicketId().isBlank() || !spot.getVehicleNumber().isBlank())
                return 404;
            if (size.get() == 1) {
                size.set(0);
                head = null;
                tail = null;
                return 201;
            }

            // if spot is head
            if (spot.getPrev() == null) {
                head = head.getNext();
                head.setPrev(null);
                size.decrementAndGet();
                return 201;
            }

            // if spot is tail
            if (spot.getNext() == null) {
                tail = tail.getPrev();
                tail.setNext(null);
                size.decrementAndGet();
                return 201;
            }

            // if spot is in middle
            ParkingSpot prev = spot.getPrev();
            ParkingSpot next = spot.getNext();
            prev.setNext(next);
            next.setPrev(prev);
            size.decrementAndGet();
        }
        return 201;
    }

    int size(){
        return size.get();
    }

}

class ParkingSpot {
    private ParkingSpot prev,next;
    private String spotId;
    private int vehicleType;
    private boolean isActive;
    private String vehicleNumber, ticketId;

    ParkingSpot(String spotId, int vehicleType, boolean isActive){
        this.spotId = spotId;
        setVehicleType(vehicleType);
        setActive(isActive);
        setPrev(null);
        setNext(null);
        setVehicleNumber("");
        setTicketId("");
    }

    public String getSpotId() {
        return spotId;
    }

    public ParkingSpot getPrev() {
        return prev;
    }

    public void setPrev(ParkingSpot prev) {
        this.prev = prev;
    }

    public ParkingSpot getNext() {
        return next;
    }

    public void setNext(ParkingSpot next) {
        this.next = next;
    }

    public int getVehicleType() {
        return vehicleType;
    }

    public void setVehicleType(int vehicleType) {
        this.vehicleType = vehicleType;
    }

    public boolean isActive() {
        return isActive;
    }

    public void setActive(boolean active) {
        isActive = active;
    }

    public String getVehicleNumber() {
        return vehicleNumber;
    }

    public void setVehicleNumber(String vehicleNumber) {
        this.vehicleNumber = vehicleNumber;
    }

    public String getTicketId() {
        return ticketId;
    }

    public void setTicketId(String ticketId) {
        this.ticketId = ticketId;
    }
}

class BookingData {
    private ConcurrentHashMap<String, ParkingResult> data;
    BookingData(){
        data = new ConcurrentHashMap<String, ParkingResult>();
    }

    void add(ParkingResult result){
        if(!result.getTicketId().isBlank())
            data.put("T$"+result.getTicketId(), result);
        if(!result.getVehicleNumber().isBlank())
            data.put("V$"+result.getVehicleNumber(), result);
    }

    ParkingResult search( String vehicleNumber, String ticketId){
        if(!ticketId.isBlank()) return data.get("T$"+ticketId);
        return data.get("V$"+vehicleNumber);
    }

}