Requirements

Determine the different ways the system will be used. This includes main functions the system needs to perform and who will use it.


Functional:

  • db:
    • have different sizes
    • record capacity and availbility
  • server:
    • assign the spot based on the car size
    • reassignment if no availbility/size not fit
    • calculate the price
    • give the parking ticket and handle checkout
    • alert overdue parking or remaining time
  • clients:
    • should be able to book a slot
    • checkin and checkout payment

Nonfunctional:

  • parking slot assignment and reassignment should be returned within 500ms
  • throughput: able to handle 100 concurrent user
  • able to scale horizontally for peek and off peek
  • able to scale vertically for possible renovation
  • data security


Define Core Objects

Based on the requirements and use cases, identify the main objects of the system...

Core Object:

  • vehilcle: vehicle id, userId, license plate, size, state
  • user: user id, name, phone #, email, Set<String> vehicle id
  • parking spot: spot id, size, status, floor number, vehicle id?, last update
  • payment: payment id, check in time, expiredTime, check out time?, unit price, amount? , transaction id? from third party payment system, payment status, user id, spot size
  • alert: alert id, message (overdue, half hour, availbility), user id, timestamp, isResolved
  • stat - half hour/1hr: statId, reportDate, small Availbility, mediumAavailability, largeAvailability, totalFillingRate, smallFillingRate, float mediumFillingRate, largeFillingRate, smallRevenue, mediumRevenue, largeRevenue, totalRevenue , checkin, checkout, average rearrangement, datetime startTime, dateTime endTime


Architecture:

Front end: mobile app

alert system: ios alert and firebase

back end server

db: sql

payment: stripe, local machine but i dont know much about it

server site stat: desktop application



Analyze Relationships

Determine how these objects will interact with each other to fulfill the use cases...

key relationship: parking spot assigned to vehicle, vehicle belongs to user, alert notified to user, alert related to payment, payment made by user, all consumption creates logging, logging feeded to external microservice and created stat


cardinality: user:vehicle = one to many; vehicle:user = many to one; spot:vehicle = one to one; user:payment = one to one; alert:user = many to many


data flow:

  • upon user enters the parking lot, user is created or fetched, created an emepty set of vehicle id
  • as user signed in vehicle type and size, create vehicle subject, and update the vehicle set in user, server check the availbility of the exact size, fetched from redis with a ttl of 5 min and assigned the parking spot, async marked the parking spot as pending
  • create alert to notify the user
  • if the spot is unavailable or size not fit, ask user if they park at different place
    • if they do, ask for spot id and find the spot size, and mark the spot as booked
    • if they dont, fetch from redis and reassign the spot id, change availbility to availbile or booked, also update redis
  • create payment object, set payment status as false
  • chron job to check the payment system, when 30 min close to expiredTime and status still false, create alert object
  • when user plans to checkout, update checkin time and do the calculation, ask for 3rd party to create transaction, create alert and update the transaction id
  • as transaction finished, marked payment status
  • set the spot to available in redis with a 5 min ttl, add a listener to the expiration and trigger the db update after the time, which means it will mark the spot back to availbile after 5 min
  • have a chron job to check the lastUpdate of the parking spot, if its more than 5 min and its still pending, update it as available
  • the chrone job should also check the total available and availability of different size
  • redis will have a list of availability of different size, so server dont need to query db everytime
  • each step create logging, and feed to microservice every 30 min, and create report
  • at 3am or sth, create daily report
  • every month, create monthly report, average the stat and check user for active user, new user



Establish Hierarchy

Design inheritance trees where applicable to promote code reuse and polymorphism. This step involves identifying common attributes and behaviors that can be abstracted into parent classes...


they shared id, createdAt, LastUpdate, as the parent object


Design Patterns

Consider using design patterns (e.g., Factory, Singleton, Observer, Strategy) that fit the problem...


Factory Pattern:

addUser(), removeUser(),

addVehicle(), removeVehicle(),


Singleton Pattern:

updateAvailability()

createPayment()

processPayment()

updateStatus()


Strategy Pattern:

feeCalculation()


observer pattern:

update availability <-> pending after ttl

logging generation and stat generation

error generation


command pattern:

check in

check out

generate alert


public abstract class BaseEntity { protected String id; protected LocalDateTime createdTime; protected LocalDateTime updatedTime; public BaseEntity() { this.id = UUID.randomUUID().toString(); this.createdTime = LocalDateTime.now(); this.updatedTime = LocalDateTime.now(); } public void updateTimestamp() { this.updatedTime = LocalDateTime.now(); } } public class User Extends BaseEntity { private String userId; private String name; private String phoneNumber; private String email; private Set<String> vehicleSet; public User(String name, String phoneNumber, String email) { super(); this.userId = id; this.phoneNumber = phoneNumber; this.email = email; vehicleSet = new HashSet<>(); } public String getUserId() { return userId; } public void addVehicle(String vehicleId) { this.vehicleSet.add(vehicleId); } public void removeVehicle(String vehicleId) { this.vehicleSet.remove(vehicleId); } public Set<String> getVehicles (String userId) { return userId.vehicleSet; } } public class Vehicle extends BaseEntity { private String vehicleId; private String userId; private String licensePlate; private String size; public Vehicle(String userId, String licensePlate, String size) { super(); this.vehicleId = id; this.userId = userId; this.licensePlate = licensePlate; this.size = size; } public String getVehicleId() { return vehicleId; } public String getUserId() { return vehicle.userId; } } public class ParkingSpot extends BaseEntity { private String spotId; private String size; private int availability; // -1 booked, 0 pending, 1 available private int floorNumber; private LocalDateTime lastUpdate; private String vehicleId; public ParkingSpot(String size, int floorNumber) { super(); this.spotId = size + id; this.size = size; this.floorNumber = floorNumber; this.availability = 1; // Default to available } public void assignVehicle(String vehicleId) { this.vehicleId = vehicleId; this.availability = 0; // Mark as pending lastUpdate = LocalDateTime.now(); } public void bookVehicle(String vehicleId) { this.vehicleId = vehicleId; this.availability = -1; // Mark as booked lastUpdate = LocalDateTime.now(); } public void releaseSpot() { this.vehicleId = null; this.availability = 1; // Mark as available lastUpdate = LocalDateTime.now(); } } public class UserFactory { private Connection connection; private VehicleFactory vehicleFactory; private User user; public UserFactory(Connection connection, VehicleFactory vehicleFactory) { this.connection = connection; this.vehicleFactory = vehicleFactory; } public User addUser(String name, String phoneNumber, String email) { User user = new User(name, phoneNumber, email); SaveToDatabase(user); return user; } public void deleteUser(String userId) { deleteFromDatase(userId); } private void saveToDatabase(User user) { String sql = "INSERT INTO Users (userId, name, phoneNumber, email, createdAt, lastUpdate) VALUES (?, ?, ?, ?, ?, ?)"; try () { System.out.println("User saved to database: " + user.getUserId()); } catch (Exception e) { e.printStackTrace(); } } private void deleteFromDatabase(String userId) { vehicleFactory.deleteVehicles(user.getVehicles()); // Now delete the user String sql = "DELETE FROM Users WHERE userId = ?"; try () { System.out.println("User deleted from database: " + user.getUserId()); } catch (SQLException e) { e.printStackTrace(); } } } public class VehicleFactory { private Connection connection; private Vehicle vehicle; public VehicleFactory(Connection connection) { this.connection = connection; } public Vehicle addVehicle(String userId, String licensePlate, String size) { Vehicle vehicle = new Vehicle(userId, licensePlate, size); saveToDatabase(vehicle); return vehicle; } public void removeVehicle(String vehicleId) { User user = vehicle.getUserId(); user.removeVehicle(vehicleId); deleteFromDatabase(vehicleId); } public void removeVehicles(Set<String> vehicleSet) { for (String v : vehicleSet) { removeVehicle(v); } } private void saveToDatabase(Vehicle vehicle) { String sql = "INSERT INTO Vehicles (vehicleId, userId, licensePlate, size, createdAt, lastUpdate) VALUES (?, ?, ?, ?, ?, ?)"; try () { System.out.println("Vehicle saved to database: " + vehicle.getVehicleId()); } catch (SQLException e) { e.printStackTrace(); } } private void deleteFromDatabase(String vehicleId) { String sql = "DELETE FROM Vehicles WHERE vehicleId = ?"; try () { System.out.println("Vehicle deleted from database: " + vehicleId); } catch (SQLException e) { e.printStackTrace(); } } } public class Payment extends BaseEntity { private String paymentId; private String spotSize; private LocalDateTime checkInTime; private LocalDateTime expiredTime; private LocalDateTime checkOutTime; private float unitPrice; private float amount; private String transactionId; private boolean paymentStatus; public Payment(String paymentId, String spotSize, LocalDateTime checkInTime, LocalDateTime expiredTime, float unitPrice) { super(); this.paymentId= paymentId; this.spotSize = spotSize; this.checkInTime = checkInTime; this.expiredTime = expiredTime; this.unitPrice = unitPrice; this.paymentStatus = false; } public void completePayment(float amount, String transactionId) { this.amount = amount; this.transactionId = transactionId; this.paymentStatus = true; this.updateTimestamp(); } } public class Alert extends BaseEntity { private String message; private String userId; private LocalDateTime timestamp; private boolean isResolved; private String transactionId; public Alert(String message, String userId) { super(); this.message = message; this.userId = userId; this.isResolved = false; this.timestamp = LocalDateTime.now(); } public void resolveAlert() { this.isResolved = true; this.updateTimestamp(); } } public class Stat extends BaseEntity { private LocalDate reportDate; private String smallAvailability; private String mediumAvailability; private String largeAvailability; private float totalFillingRate; private float totalRevenue; private int checkin; private int checkout; public Stat(LocalDate reportDate) { super(); this.reportDate = reportDate; } public void updateStats(String smallAvail, String mediumAvail, String largeAvail, float fillingRate, float revenue) { this.smallAvailability = smallAvail; this.mediumAvailability = mediumAvail; this.largeAvailability = largeAvail; this.totalFillingRate = fillingRate; this.totalRevenue = revenue; this.updateTimestamp(); } } public class FeeStrategy { public double feeCalculation(String spotSize, LocalDateTime checkIn, LocalDateTime checkOut) { double ratePerHour = getRateForSize(spotSize); long hours = Duration.between(checkIn, checkOut).toHours(); return ratePerHour * Math.max(hours, 1); // At least 1 hour charged } private double getRateForSize(String size) { switch (size) { case "small": return 2.5; case "medium": return 3.5; case "large": return 5.0; default: throw new IllegalArgumentException("Invalid spot size"); } } } public class Command { private LocalDateTime updatedTime = LocalDateTime.now(); public void assign(String spotId, String vehicleId, String userId) { String sql = "UPDATE ParkingSpot SET availability = 0, lastUpdate = ?, vehicleId = ? WHERE spotId = ?"; String sql = "UPDATE User SET lastUpdate = ? WHERE userId = ?"; try { System.out.println("Assign updated in database for spot: " + spotId); } catch (SQLException e) { e.printStackTrace(); } } public void checkin(String spotId, String vehicleId, String userId) { this.vehicleId = vehicleId; this.userId = vehicleId.getUserId(); String sql = "UPDATE ParkingSpot SET availability = -1, vehicleId = ?, lastUpdate = ? WHERE spotId = ?"; String sql = "UPDATE User SET lastUpdate = ? WHERE userId = ?"; try { System.out.println("Check-in updated in database for spot: " + spotId); } catch (SQLException e) { e.printStackTrace(); } } public void checkout(String spotId, String userId) { String sql = "UPDATE ParkingSpot SET availability = 1, vehicleId = NULL, lastUpdate = ? WHERE spotId = ?"; String sql = "UPDATE User SET lastUpdate = ? WHERE userId = ?"; try { System.out.println("Check-out updated in database for spot: " + spotId); } catch (SQLException e) { e.printStackTrace(); } } public void updateAvailability(String spotId, int availability) { String sql = "UPDATE ParkingSpot SET availability = ?, lastUpdate = ? WHERE spotId = ?"; try { System.out.println("Availability updated in database for spot: " + spotId); } catch (SQLException e) { e.printStackTrace(); } } } public class PaymentService { public Payment createPayment(String spotId, String spotSize, LocalDateTime checkIn, LocalDateTime checkoutTime, double unitPrice) { Payment payment = new Payment(spotId, spotSize, checkIn, checkoutTime, unitPrice); saveToDatabase(payment); return payment; } public void processPayment(Payment payment, String transactionId) { payment.setTransactionId(transactionId); payment.setPaymentStatus(true); updateStatus(payment); } public void updateStatus(Payment payment) { String sql = "UPDATE Payment SET transactionId = ?, paymentStatus = ?, checkOutTime = ?, updatedTime = ? WHERE paymentId = ?"; try { System.out.println("Payment status updated in database for payment ID: " + payment.getPaymentId()); } catch (SQLException e) { e.printStackTrace(); } } private void saveToDatabase(Payment payment) { String sql = "INSERT INTO Payment (paymentId, spotId, spotSize, checkInTime, expiredTime, unitPrice, amount, paymentStatus, createdTime, updatedTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; try { System.out.println("Payment saved to database: " + payment.getPaymentId()); } catch (SQLException e) { e.printStackTrace(); } } }




Adhere to SOLID Guidelines

Check and explain whether your design adheres to solid principles (Ask interviewer what SOLID principle is if you can not recall it.)...

S: single responsibility: each class should have standalone responsibility

O: open/closed principle: if i need new parking strategy, i can add a standalone strategy class to assign the strategy, and if i need to change fee calculation, i can change the fee calculation strategy class

L: subclass cannot intervene top class: all payment related operations are handled within payment service

I: Interface segregation principle: my payment system handles creating processing payment and update status; fee class handles fee calculation; and command class handles assign check in and checkout parking spot and update availbility of the spot; and user and vehicle factory create and delete user; so they only contains useful functions

D: yea i have the high level and low level module




Consider Scalability and Flexibility

Explain how your design can handle changes in scale and whether it would be easily to extend with new functionalities...

check my high level diagram for service granulility, load balancing and cache service

I use redis to fetch a list of different sizes of parking lots, so the system can repsond to parking lot assignment in 500 ms even during peek time.

if i need to renovate the parking lot, i can add new spot to the parking spot db with new floor number, size or others

the spot assignment, fee caulcation and payment are standalone features, so if its in high traffic, i can scale horizontally to match the need

the alert system is handled through microservice, so if there are more need, we can scale if separately

i have the logging service for health check and analytics so i can increase the instance based on recent activity load



Create/Explain your diagram(s)

Try creating a class, flow, state and/or sequence diagram using the diagramming tool. Mermaid flow diagrams can be used to represent system use cases. You can ask the interviewer bot to create a starter diagram if unfamiliar with the tool. Briefly explain your diagrams if necessary...


i will use redis to reserve the spot and have it ttl of 5 min,

and if the client checkout, also hold a 5 min ttl to change it back to available


the db are sharded on diff db, so if part of the spot isnt available, my payment class can still use spot size and do the calculation, to correctly know the spot size even if the spot id isnt accessible, i will add the size identifer to the start of the spot id




Future improvements

Critically examine your design for any flaws or areas for future improvement...