Requirements


Functional Requirements:


  • Manage vehicle parking across multiple floors.
  • Parking spot assignment based on vehicle size.
  • Spot availability checking.
    • If spots available, assign a random spot.
    • If spots full, tell user there are no spots available and to join a waitlist.
    • Create waitlist queue.
  • Create Ticket creation and ticket processing service.
  • Fee calculation based on parking duration. Fee's are calculated as follows:
    • 0-1 hour: $1
    • 2-3 hours: $3
    • 3-5 hours: $6
    • 5-9 hours: $9
    • 9+ hours: $24 (assume full 24 hours)


Non-Functional Requirements:

  • Handle ticket assignment race conditions - ie: 2 drivers both want spot A, both can't be assigned the same spot. Solution: 1 synchronized method for inserting driver spot permission and 1 synchronized method for deleting driver spot permission. Use a Concurrent Hashmap to handle fast reads and updates from users.
  • Handle extensibility for operations like payment methods through the adapter design pattern.


Core Objects & Relationships

Based on the requirements and use cases, identify the main objects of the system and analyze how they interact and relate to each other...


Here's a Class Diagram illustrating the relationships of the core objects and too each other:





APIs & Class Members

For each class, define the attributes (data) it will hold and the methods (functions) that operate on the attributes. Ensure they align with the object's responsibilities and adhere to the principle of encapsulation. Write your code in the code editor below.


import java.math.BigDecimal;

import java.time.Duration;

import java.time.LocalDateTime;

import java.util.*;

import java.util.concurrent.ConcurrentHashMap;

import java.util.concurrent.ConcurrentLinkedQueue;

import java.util.concurrent.atomic.AtomicInteger;


enum VehicleType {

MOTORCYCLE, COMPACT, REGULAR, LARGE, EV

}


enum ParkingSpotType {

MOTORCYCLE, COMPACT, REGULAR, LARGE, EV

}


enum TicketStatus {

OPEN, CLOSED, LOST

}


class Driver {

private String firstName;

private String lastName;

private int age;

private String email;


public Driver(String firstName, String lastName, int age, String email) {

this.firstName = firstName;

this.lastName = lastName;

this.age = age;

this.email = email;

}

}


class Vehicle {

private String make;

private String model;

private int year;

private VehicleType vehicleType;

private Driver driver;

private String licensePlateNumber;


public Vehicle(String make, String model, int year, VehicleType vehicleType,

Driver driver, String licensePlateNumber) {

this.make = make;

this.model = model;

this.year = year;

this.vehicleType = vehicleType;

this.driver = driver;

this.licensePlateNumber = licensePlateNumber;

}


public VehicleType getVehicleType() {

return vehicleType;

}


public String getLicensePlateNumber() {

return licensePlateNumber;

}

}


class ParkingSpot {

private final int spotNum;

private final ParkingSpotType parkingSpotType;

private Vehicle vehicle;

private boolean assigned;


public ParkingSpot(int spotNum, ParkingSpotType parkingSpotType) {

this.spotNum = spotNum;

this.parkingSpotType = parkingSpotType;

this.assigned = false;

}


public synchronized boolean tryAssignVehicle(Vehicle vehicle) {

if (!canFitVehicle(vehicle)) {

return false;

}


this.vehicle = vehicle;

this.assigned = true;

return true;

}


public synchronized void removeVehicle() {

this.vehicle = null;

this.assigned = false;

}


public synchronized boolean canFitVehicle(Vehicle vehicle) {

if (assigned) return false;


VehicleType type = vehicle.getVehicleType();


return switch (parkingSpotType) {

case MOTORCYCLE -> type == VehicleType.MOTORCYCLE;

case COMPACT -> type == VehicleType.MOTORCYCLE || type == VehicleType.COMPACT;

case REGULAR -> type == VehicleType.MOTORCYCLE

|| type == VehicleType.COMPACT

|| type == VehicleType.REGULAR;

case LARGE -> type == VehicleType.MOTORCYCLE

|| type == VehicleType.COMPACT

|| type == VehicleType.REGULAR

|| type == VehicleType.LARGE;

case EV -> type == VehicleType.EV;

};

}


public int getSpotNum() {

return spotNum;

}


public ParkingSpotType getParkingSpotType() {

return parkingSpotType;

}

}


class ParkingFloor {

private final int floorNum;

private final List spots;


public ParkingFloor(int floorNum) {

this.floorNum = floorNum;

this.spots = new ArrayList<>();

}


public void addSpot(ParkingSpot spot) {

spots.add(spot);

}


public Optional assignAvailableSpot(Vehicle vehicle) {

for (ParkingSpot spot : spots) {

if (spot.tryAssignVehicle(vehicle)) {

return Optional.of(spot);

}

}


return Optional.empty();

}


public int getFloorNum() {

return floorNum;

}

}


class Ticket {

private final int ticketId;

private final LocalDateTime checkIn;

private LocalDateTime checkOut;

private TicketStatus status;

private BigDecimal fee;

private final ParkingSpot spot;

private final Vehicle vehicle;


public Ticket(int ticketId, ParkingSpot spot, Vehicle vehicle) {

this.ticketId = ticketId;

this.spot = spot;

this.vehicle = vehicle;

this.checkIn = LocalDateTime.now();

this.status = TicketStatus.OPEN;

this.fee = BigDecimal.ZERO;

}


public void close(BigDecimal fee) {

this.checkOut = LocalDateTime.now();

this.status = TicketStatus.CLOSED;

this.fee = fee;

this.spot.removeVehicle();

}


public int getTicketId() {

return ticketId;

}


public LocalDateTime getCheckIn() {

return checkIn;

}


public TicketStatus getStatus() {

return status;

}


public BigDecimal getFee() {

return fee;

}


public ParkingSpot getSpot() {

return spot;

}


public Vehicle getVehicle() {

return vehicle;

}

}


interface PricingStrategy {

BigDecimal calculate(Ticket ticket);

}


class DurationBasedPricingStrategy implements PricingStrategy {

@Override

public BigDecimal calculate(Ticket ticket) {

long minutes = Duration.between(ticket.getCheckIn(), LocalDateTime.now()).toMinutes();

long hours = (long) Math.ceil(minutes / 60.0);


if (hours <= 1) return BigDecimal.valueOf(1);

if (hours <= 3) return BigDecimal.valueOf(3);

if (hours <= 5) return BigDecimal.valueOf(6);

if (hours <= 9) return BigDecimal.valueOf(9);


return BigDecimal.valueOf(24);

}

}


class TicketService {

private final ConcurrentHashMap tickets = new ConcurrentHashMap<>();

private final Queue waitList = new ConcurrentLinkedQueue<>();

private final PricingStrategy pricingStrategy;

private final AtomicInteger ticketIdGenerator = new AtomicInteger(1);


public TicketService(PricingStrategy pricingStrategy) {

this.pricingStrategy = pricingStrategy;

}


public Ticket createTicket(ConcurrentHashMap floors, Vehicle vehicle) {

for (ParkingFloor floor : floors.values()) {

Optional spotOptional = floor.assignAvailableSpot(vehicle);


if (spotOptional.isPresent()) {

ParkingSpot spot = spotOptional.get();

int ticketId = ticketIdGenerator.getAndIncrement();


Ticket ticket = new Ticket(ticketId, spot, vehicle);

tickets.put(ticketId, ticket);


return ticket;

}

}


waitList.add(vehicle);

throw new IllegalStateException("No available parking spots. Vehicle added to waitlist.");

}


public Ticket closeTicket(int ticketId) {

Ticket ticket = tickets.get(ticketId);


if (ticket == null) {

throw new IllegalArgumentException("Ticket not found.");

}


if (ticket.getStatus() != TicketStatus.OPEN) {

throw new IllegalStateException("Ticket is not open.");

}


BigDecimal fee = pricingStrategy.calculate(ticket);

ticket.close(fee);


alertSpotOpen();


return ticket;

}


private void alertSpotOpen() {

Vehicle nextVehicle = waitList.peek();


if (nextVehicle != null) {

System.out.println("Spot opened. Next vehicle in waitlist: "

+ nextVehicle.getLicensePlateNumber());

}

}

}


class ParkingGarage {

private final ConcurrentHashMap floors = new ConcurrentHashMap<>();

private final TicketService ticketService;


public ParkingGarage(TicketService ticketService) {

this.ticketService = ticketService;

}


public void addFloor(ParkingFloor floor) {

floors.put(floor.getFloorNum(), floor);

}


public Ticket createTicket(Vehicle vehicle) {

return ticketService.createTicket(floors, vehicle);

}


public Ticket closeTicket(int ticketId) {

return ticketService.closeTicket(ticketId);

}

}


public class Main {

public static void main(String[] args) {

PricingStrategy pricingStrategy = new DurationBasedPricingStrategy();

TicketService ticketService = new TicketService(pricingStrategy);

ParkingGarage garage = new ParkingGarage(ticketService);


ParkingFloor floor1 = new ParkingFloor(1);

floor1.addSpot(new ParkingSpot(101, ParkingSpotType.COMPACT));

floor1.addSpot(new ParkingSpot(102, ParkingSpotType.REGULAR));

floor1.addSpot(new ParkingSpot(103, ParkingSpotType.LARGE));

floor1.addSpot(new ParkingSpot(104, ParkingSpotType.EV));


garage.addFloor(floor1);


Driver driver = new Driver("Arin", "Mauk", 29, "[email protected]");


Vehicle vehicle = new Vehicle(

"Honda",

"Civic",

2020,

VehicleType.COMPACT,

driver,

"ABC-123"

);


Ticket ticket = garage.createTicket(vehicle);


System.out.println("Ticket created: " + ticket.getTicketId());

System.out.println("Spot assigned: " + ticket.getSpot().getSpotNum());


Ticket closedTicket = garage.closeTicket(ticket.getTicketId());


System.out.println("Ticket closed.");

System.out.println("Fee: $" + closedTicket.getFee());

}

}




Deep Dive

Explain design tradeoffs you considered. Check and explain whether your design adheres to SOLID principles. Explain how your design can handle changes in scale and whether it would be easy to extend with new functionalities. Identify areas for future improvement...


Design Tradeoffs:

  • I chose a simple object model with ParkingGarage, ParkingFloor, ParkingSpot, TicketService, Ticket, vehicle and driver because it maps closely to the real-world domain.
  • The biggest tradeoff is simplicity vs flexibility
    • For example, fee calculation could be a simple method on Ticket, but I separated it into a PricingStrategy. For the current rule set, that may be slightly overengineered, but it makes the design easier to extend later for vehicle-based pricing, event pricing, weekend pricing, or dynamic pricing.
  • Another tradeoff is concurrency granularity. Synchronizing the entire TicketService is simple and safe, but limits throughput. A better scalable design is to make individual ParkingSpot assignment atomic so multiple floors/spots can be processed concurrently.


Solid Principles:

  • Single Responsibility Principle
    • ParkingGarage manages floors and delegates ticket operations.
    • TicketService handles ticket creation and closing.
    • ParkingSpot manages spot availability and assignment.
    • PricingStrategy handles fee calculation.
    • One improvement: TicketService currently does both ticket lifecycle management and spot-finding logic. In a larger system, I would extract spot search into a separate ParkingSpotAllocationService.
  • Open/Closed Principle:
    • yes, such as pricing. Pricing is behind a pricingStrategy interface making VehicleTypePricingStrategy, EventPricingStrategy, etc, without chaning TicketService.
  • Liskov Substitution Principle:
    • Any implementation of PricingStrategy can replace another as long as it returns a valid fee for a ticket.
  • Interface Segregation Principle:

Yes. PricingStrategy is small and focused: BigDecimal calculate(Ticket ticket);

    • No class is forced to implement methods it does not need.

Future improvement:

  • The biggest improvements I would make are:
    • Extract spot allocation into a separate service or strategy.
    • Add a repository layer.
    • Improve concurrency with spot-level locking instead of service-level locking.
    • Add payment processing.
    • Replace hardcoded vehicle compatibility rules with a policy class.