Requirements
Determine the different ways the system will be used. This includes main functions the system needs to perform and who will use it.
-> Allot a parking spot based on availability
-> Availability of parking spots across various floor.
-> Allow based on vehicle type and availability
-> Payment System -> third party payment system like stripe or paypal
-> A user should be able to login and get the spot
Define Core Objects
Based on the requirements and use cases, identify the main objects of the system...
| Class/Interface Description | |
| ParkingLot | Entry point; coordinates spot allocation, user interaction, payment. |
| Floor | Represents floors within the parking lot. |
| ParkingSpot | Represents a parking space (availability, rate, type). |
| ParkingSpotManager | Manages parking spots (safe concurrent updates). |
| User | Represents the user interacting with the system. |
| ParkingTicket | Tracks the start and end times of parking sessions. |
| PaymentHandler | Interface for different payment gateways. |
| PaymentService | Delegates payment logic to the PaymentHandler. |
Analyze Relationships
ParkingLot delegates spot management to ParkingSpotManager, reservation handling to ReservationService, and payment to PaymentService.
ParkingSpotManager tracks and manages parking spots.
PaymentService uses the PaymentGateway interface (with a StripePaymentGateway implementation) for payments.
User holds active ParkingTickets, which link to ParkingSpots.
These objects interact to:
Assign a spot to a user.
Mark a spot as free when the user leaves.
Process payments via the payment gateway.
This design allows for easy addition of payment gateways and clear separation of responsibilities.
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...
Interfaces:
PaymentHandler is implemented by concrete payment classes like StripePaymentHandler and potentially others (e.g., PayPalPaymentHandler).
Future expansion: A ReservationService interface can handle reservations separately.
Inheritance (future extension):
If different types of ParkingSpot need distinct behavior (e.g., electric vs. regular), create subclasses like ElectricSpot extending ParkingSpot.
Polymorphism:
Payment processing leverages polymorphism through the PaymentHandler interface.
Design Patterns
Consider using design patterns (e.g., Factory, Singleton, Observer, Strategy) that fit the problem...
Strategy Pattern:
Payment gateways implement the PaymentHandler interface, allowing dynamic switching without modifying existing code.
Singleton-like SpotManager (pragmatic):
We centralize spot allocation to ParkingSpotManager, ensuring consistent state.
Factory Method (possible future improvement):
When adding new spot types or payment handlers, introduce a SpotFactory or PaymentHandlerFactory.
Define Class Members (write code)
Attributes: For each class, define the attributes (data) it will hold...
Methods: Define the methods (functions) that operate on the attributes. Ensure they align with the object's responsibilities and adhere to the principle of encapsulation.
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
enum ParkingType { COMPACT, LARGE, HANDICAPPED }
class ParkingLot {
private final String id;
private final int floors;
private final ParkingSpotManager spotManager;
private final PaymentService paymentService;
public ParkingLot(String id, int floors, PaymentHandler paymentHandler) {
this.id = id;
this.floors = floors;
this.spotManager = new ParkingSpotManager();
this.paymentService = new PaymentService(paymentHandler);
}
public ParkingTicket assignSpot(User user, ParkingType type) {
ParkingSpot spot = spotManager.getAvailableSpotByType(type);
if (spot == null) throw new RuntimeException("No spot available");
spot.setAvailable(false);
ParkingTicket ticket = new ParkingTicket(UUID.randomUUID().toString(), user, spot);
user.addTicket(ticket);
return ticket;
}
public void freeSpot(ParkingTicket ticket) {
ParkingSpot spot = ticket.getSpot();
spot.setAvailable(true);
paymentService.processPayment(ticket);
}
public void addSpot(Floor floor, double rate, ParkingType type) {
spotManager.addParkingSpot(floor, rate, type);
}
}
// ParkingSpotManager with concurrency-safe structures
class ParkingSpotManager {
private final Map<String, ParkingSpot> spots = new ConcurrentHashMap<>();
public void addParkingSpot(Floor floor, double rate, ParkingType type) {
ParkingSpot spot = new ParkingSpot(UUID.randomUUID().toString(), floor, type, rate);
spots.put(spot.getId(), spot);
}
public ParkingSpot getAvailableSpotByType(ParkingType type) {
return spots.values().stream()
.filter(s -> s.isAvailable() && s.getType() == type)
.findFirst().orElse(null);
}
public List<ParkingSpot> getAvailableSpots() {
List<ParkingSpot> available = new ArrayList<>();
for (ParkingSpot s : spots.values()) {
if (s.isAvailable()) available.add(s);
}
return available;
}
}
class PaymentService {
private final PaymentHandler handler;
public PaymentService(PaymentHandler handler) {
this.handler = handler;
}
public void processPayment(ParkingTicket ticket) {
if (!handler.processPayment(ticket)) {
throw new RuntimeException("Payment failed");
}
}
}
interface PaymentHandler {
boolean processPayment(ParkingTicket ticket);
}
class StripePaymentHandler implements PaymentHandler {
@Override
public boolean processPayment(ParkingTicket ticket) {
double amount = ticket.calculateTotalFee();
System.out.println("Stripe payment processed: $" + amount);
return true;
}
}
class User {
private final String id;
private final String name;
private final List<ParkingTicket> activeTickets = Collections.synchronizedList(new ArrayList<>());
public User(String id, String name) {
this.id = id;
this.name = name;
}
public void addTicket(ParkingTicket ticket) {
activeTickets.add(ticket);
}
}
class Floor {
private final String id;
private final int floorNo;
public Floor(String id, int floorNo) {
this.id = id;
this.floorNo = floorNo;
}
public int getFloorNo() { return floorNo; }
}
class ParkingSpot {
private final String id;
private final Floor floor;
private final ParkingType type;
private final double rate;
private volatile boolean isAvailable;
private final ReentrantLock lock = new ReentrantLock();
public ParkingSpot(String id, Floor floor, ParkingType type, double rate) {
this.id = id;
this.floor = floor;
this.type = type;
this.rate = rate;
this.isAvailable = true;
}
public String getId() { return id; }
public ParkingType getType() { return type; }
public boolean isAvailable() { return isAvailable; }
public void setAvailable(boolean available) {
lock.lock();
try {
this.isAvailable = available;
} finally {
lock.unlock();
}
}
public double getRate() { return rate; }
}
class ParkingTicket {
private final String id;
private final LocalDateTime startTime;
private final User user;
private final ParkingSpot spot;
private LocalDateTime endTime;
public ParkingTicket(String id, User user, ParkingSpot spot) {
this.id = id;
this.startTime = LocalDateTime.now();
this.user = user;
this.spot = spot;
}
public void endParking() { this.endTime = LocalDateTime.now(); }
public ParkingSpot getSpot() { return spot; }
public double calculateTotalFee() {
endParking();
Duration duration = Duration.between(startTime, endTime);
long hours = duration.toHours();
if (hours == 0) hours = 1; // Minimum 1 hour
return hours * spot.getRate();
}
}
public class Main {
public static void main(String[] args) {
// Setup
PaymentHandler paymentHandler = new StripePaymentHandler();
ParkingLot lot = new ParkingLot("Lot1", 3, paymentHandler);
Floor floor1 = new Floor("F1", 1);
lot.addSpot(floor1, 10.0, ParkingType.COMPACT);
// Use case
User user = new User("U1", "John Doe");
ParkingTicket ticket = lot.assignSpot(user, ParkingType.COMPACT);
System.out.println("Spot assigned: " + ticket.getSpot().getId());
// Release the spot and pay
lot.freeSpot(ticket);
}
}
Adhere to SOLID Guidelines
Single Responsibility Principle (SRP): Each class has only one job. For example, the ParkingLot class handles the overall parking logic, and the ParkingSpot handles individual spots.
Open/Closed Principle (OCP): You can extend the system (like adding new spot types or payment processors) without modifying existing code.
Liskov Substitution Principle (LSP): The small and big parking spots inherit from ParkingSpot and can be used wherever a ParkingSpot is expected.
Interface Segregation Principle (ISP): Interfaces like IPaymentProcessor only have relevant methods, not unnecessary ones.
Dependency Inversion Principle (DIP): The ParkingLot depends on IPaymentProcessor instead of a concrete implementation, which makes it flexible and easy to switch payment processors.
Consider Scalability and Flexibility
Scalability
Adding more parking spots, floors, or even entire buildings is straightforward because each spot and floor is encapsulated.
Adding more users and managing them is easy since the system uses a map of active tickets, so even thousands of users can be handled efficiently.
Because each component is small and focused, it can be tested and optimized separately as the number of users grows.
Flexibility
Adding new parking spot types (like electric vehicle spots, VIP spots) just means creating new subclasses of ParkingSpot.
Supporting new payment processors (like mobile payments or new payment gateways) only requires creating another implementation of IPaymentProcessor—no changes to the ParkingLot class are needed.
Adding features like reservations, time-based discounts, or loyalty programs would involve extending the ticket and spot classes or adding new services without breaking existing logic.
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...
Future improvements
Scalability of ParkingSpotManager
If the number of parking spots grows large, storing them in a simple Map might not be optimal for searching or filtering. Could consider indexing or database-backed storage.
ReservationService missing
In the current diagram, ReservationService has no details. Define its responsibilities—like holding reservations, managing overlaps, or tracking expiry.
Handling concurrency
Concurrency is not addressed. For example, two users might try to book the same spot simultaneously. Could add thread-safe data structures or a database transaction layer.
Vehicle types and spot mapping
You might want to introduce a Vehicle class with type-specific rules for spot assignment (e.g., electric vehicles needing charging spots).
Extensibility of PaymentService
Currently, only StripePaymentGateway is implemented. For future expansion, consider a factory pattern or service locator to support multiple gateways dynamically.
User roles & permissions
If you have different user roles (admin, guest), consider using a role-based system rather than a generic User class.
Better error handling
The current design lacks error handling (e.g., invalid payments, full parking lot). Define robust error models or exceptions.
Testing and maintainability
Consider adding interfaces or abstract classes for core services (like ParkingService) to make them easier to mock and test.