Design patterns to be used in car rental service
A Car Rental Service can benefit greatly from using several software design patterns to handle various features and requirements.
1. Singleton Pattern
Use Case:
For classes that manage global state or configurations, such as
BranchManager
,NotificationService
, orDatabaseConnection
.class BranchManager { private static BranchManager instance; private BranchManager() { // Private constructor } public static BranchManager getInstance() { if (instance == null) { synchronized (BranchManager.class) { if (instance == null) { instance = new BranchManager(); } } } return instance; } }
Use of
static
in Singleton ImplementationIn your Singleton Pattern implementation,
static
is used for two main reasons:Static Instance (
static BranchManager instance
)Ensures only one instance is shared across the entire application.
Belongs to the class, not an instance, so it is initialized once and shared globally.
Static Method (
static getInstance()
)Allows access to the single instance without needing to instantiate the class.
You can call
BranchManager.getInstance()
without creating an object.
Why
static
forgetInstance()
?✅ A non-static method would require an instance to call
getInstance()
, which defeats the Singleton purpose.
✅ Static methods can be accessed without creating an object:
Why static
for instance
?
✅ Ensures that only one copy of instance
exists for all objects.
✅ Without static
, every object would have its own copy, breaking the Singleton pattern.
2. Factory Pattern
Use Case:
To create different types of vehicles (e.g.,
Car
,Truck
,SUV
,Motorcycle
) without exposing the instantiation logic.
Example:
class VehicleFactory {
public static Vehicle createVehicle(VehicleType type, String vehicleId, String licensePlate,
String model, String manufacturer, ParkingStall parkingStall) {
switch (type) {
case CAR:
return new Car(vehicleId, licensePlate, model, manufacturer, parkingStall, VehicleStatus.AVAILABLE);
case TRUCK:
return new Truck(vehicleId, licensePlate, model, manufacturer, parkingStall, VehicleStatus.AVAILABLE);
case SUV:
return new SUV(vehicleId, licensePlate, model, manufacturer, parkingStall, VehicleStatus.AVAILABLE);
default:
throw new IllegalArgumentException("Invalid vehicle type");
}
}
}
Vehicle car = VehicleFactory.createVehicle(VehicleType.CAR, "V1", "XYZ123", "ModelX", "Tesla", parkingStall);
because of static we don't need to create object for above class
3. Builder Pattern
Use Case:
For creating complex objects like
Reservation
, where multiple optional attributes (e.g., additional services, insurance) need to be configured.
class Reservation {
private String reservationId;
private Vehicle vehicle;
private Member member;
private Date pickupDate;
private Date dueDate;
private List<AdditionalService> additionalServices;
private Reservation(ReservationBuilder builder) {
this.reservationId = builder.reservationId;
this.vehicle = builder.vehicle;
this.member = builder.member;
this.pickupDate = builder.pickupDate;
this.dueDate = builder.dueDate;
this.additionalServices = builder.additionalServices;
}
public static class ReservationBuilder {
private String reservationId;
private Vehicle vehicle;
private Member member;
private Date pickupDate;
private Date dueDate;
private List<AdditionalService> additionalServices = new ArrayList<>();
public ReservationBuilder(String reservationId, Vehicle vehicle, Member member) {
this.reservationId = reservationId;
this.vehicle = vehicle;
this.member = member;
}
public ReservationBuilder setPickupDate(Date pickupDate) {
this.pickupDate = pickupDate;
return this;
}
public ReservationBuilder setDueDate(Date dueDate) {
this.dueDate = dueDate;
return this;
}
public ReservationBuilder addAdditionalService(AdditionalService service) {
this.additionalServices.add(service);
return this;
}
public Reservation build() {
return new Reservation(this);
}
}
}
4. Strategy Pattern
Use Case:
For dynamic pricing strategies or calculating late fees.
interface PricingStrategy { double calculatePrice(Vehicle vehicle, int rentalDuration); } class StandardPricingStrategy implements PricingStrategy { @Override public double calculatePrice(Vehicle vehicle, int rentalDuration) { return rentalDuration * 50; // $50 per hour } } class DynamicPricingStrategy implements PricingStrategy { @Override public double calculatePrice(Vehicle vehicle, int rentalDuration) { return rentalDuration * (vehicle.type == VehicleType.SUV ? 80 : 60); } } class PricingContext { private PricingStrategy strategy; public PricingContext(PricingStrategy strategy) { this.strategy = strategy; } public double executeStrategy(Vehicle vehicle, int rentalDuration) { return strategy.calculatePrice(vehicle, rentalDuration); } }
5. Observer Pattern
Use Case:
For sending notifications (e.g., upcoming pick-up date, due date reminders, or promotions).
Example:
interface Observer {
void update(String message);
}
class Member implements Observer {
private String memberId;
private String email;
public Member(String memberId, String email) {
this.memberId = memberId;
this.email = email;
}
@Override
public void update(String message) {
System.out.println("Notification sent to " + email + ": " + message);
}
}
class NotificationService {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void removeObserver(Observer observer) {
observers.remove(observer);
}
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
6. Decorator Pattern
Use Case:
For adding additional services like insurance, navigation, roadside assistance, etc., to a reservation.
Example:
interface RentalService {
double getCost();
String getDescription();
}
class BasicRental implements RentalService {
@Override
public double getCost() {
return 100; // Base cost
}
@Override
public String getDescription() {
return "Basic Rental";
}
}
class InsuranceDecorator implements RentalService {
private RentalService rentalService;
public InsuranceDecorator(RentalService rentalService) {
this.rentalService = rentalService;
}
@Override
public double getCost() {
return rentalService.getCost() + 20; // Add insurance cost
}
@Override
public String getDescription() {
return rentalService.getDescription() + ", Insurance";
}
}
class GPSDecorator implements RentalService {
private RentalService rentalService;
public GPSDecorator(RentalService rentalService) {
this.rentalService = rentalService;
}
@Override
public double getCost() {
return rentalService.getCost() + 15; // Add GPS cost
}
@Override
public String getDescription() {
return rentalService.getDescription() + ", GPS";
}
}
7. Chain of Responsibility Pattern
Use Case:
For handling reservation approval, cancellation, or refund requests in a structured pipeline.
Example:
abstract class RequestHandler {
protected RequestHandler nextHandler;
public void setNextHandler(RequestHandler nextHandler) {
this.nextHandler = nextHandler;
}
public abstract void handleRequest(String request);
}
class ReservationHandler extends RequestHandler {
@Override
public void handleRequest(String request) {
if (request.equals("Reservation")) {
System.out.println("Handling reservation request.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
class PaymentHandler extends RequestHandler {
@Override
public void handleRequest(String request) {
if (request.equals("Payment")) {
System.out.println("Handling payment request.");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
8. Proxy Pattern
Use Case:
For controlling access to sensitive operations, such as
DatabaseAccess
orAdminFunctions
.
Example:
interface VehicleInventory {
void updateInventory(String vehicleId);
}
class RealVehicleInventory implements VehicleInventory {
@Override
public void updateInventory(String vehicleId) {
System.out.println("Vehicle inventory updated for ID: " + vehicleId);
}
}
class VehicleInventoryProxy implements VehicleInventory {
private RealVehicleInventory realInventory;
private boolean isAdmin;
public VehicleInventoryProxy(boolean isAdmin) {
this.realInventory = new RealVehicleInventory();
this.isAdmin = isAdmin;
}
@Override
public void updateInventory(String vehicleId) {
if (isAdmin) {
realInventory.updateInventory(vehicleId);
} else {
System.out.println("Access Denied: Only admins can update inventory.");
}
}
}