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
andcolumn
each time is inefficient. Ideally, we want system to suggest us a parking spot based on just the vehicleTypei.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 toO(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 bevehicleNumber
andticketId
. So basically we are caching each key.And we use a
ConcurrentHashMap
so that we don't get aConcurrentModificationException
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);
}
}
Subscribe to my newsletter
Read articles from Prashant Priyadarshi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by