A deep dive into building RentFlow, a Python OOP system that transitions a manual car rental business into a robust and automated platform.
The Nightmare of Scaling via Spreadsheets
Every growing business eventually hits a wall where tracking operations manually becomes impossible.
For SafariDrive Rentals, a car rental company in Kenya, that wall was hit hard. They were tracking a growing fleet of vehicles, customer profiles, variable billing structures, return dates, and incoming cash flow using a combination of physical notebooks and cluttered spreadsheets.
Staff couldn't answer simple operational questions:
- Which vehicles are available right now?
- How long has a late vehicle been out, and how much penalty fees are owed?
- Has a client actually settled their invoice?
- What is our precise total revenue this month?
To solve this, I designed and built RentFlow, a production-grade command-line software platform powered by the core pillars of Object-Oriented Programming (OOP).
In this article, I’ll break down exactly how I designed the application, how the business logic operates, and how the four pillars of OOP map directly to real-world workflows.
System Architecture & Directory Structure
To keep the application modular, easy to maintain, and well decoupled, I avoided writing the whole code into a single, lengthy script. Instead, I split the enterprise logic into separate files:
rentflow/
│
├── main.py # CLI Menu Orchestrator & User Interface
├── vehicle.py # Abstract Vehicle class & Category overrides
├── customer.py # Customer profile logs & history linkages
├── rental.py # The transactional contract State Machine
├── payment.py # Financial gateway gateway subclasses
└── rental_system.py # The central Business Operations Manager Ledger
The 4 Pillars of OOP in Action
Here is a breakdown of how the four fundamental concepts of object-oriented programming saved this software from turning into unmanageable "spaghetti code."
1. Abstraction
Abstraction means defining what a component must do without exposing or hardcoding how it does it.
In vehicle.py, Vehicle is declared as an Abstract Base Class (ABC). The business rules dictate that you should never be able to initialize a raw, generic vehicle. SafariDrive only deals in actual, explicit categories.
from abc import ABC, abstractmethod
class Vehicle(ABC):
def __init__(self, registration_number, make, model, year, daily_rate):
if daily_rate < 0:
raise ValueError("Daily rate cannot be negative.")
self.registration_number = registration_number
self.make = make
self.model = model
self.year = year
self.daily_rate = daily_rate
self.__available = True # Hidden internal state
@abstractmethod
def calculate_rental_cost(self, days):
pass
If a developer tries to run Vehicle("KDK 123A", "Toyota", "Axio", 2022, 4000), Python steps in and throws a structural runtime error. That is abstraction guarding our architecture.
2. Inheritance
Inheritance lets us write code once in a parent class and automatically reuse it across multiple child classes, preventing code duplication.
Subclasses like EconomyCar, SUV, and LuxuryCar inherit everything from Vehicle via super().__init__(), but instantly implement their unique real-world variations:
class EconomyCar(Vehicle):
def calculate_rental_cost(self, days):
return self.daily_rate * days
class SUV(Vehicle):
def calculate_rental_cost(self, days):
service_charge = 2000
return (self.daily_rate * days) + service_charge
class LuxuryCar(Vehicle):
def calculate_rental_cost(self, days):
base = self.daily_rate * days
return base + (base * 0.10) # Includes a mandatory 10% insurance premium
3. Polymorphism
Polymorphism allows our main system to treat different objects uniformly.
When generating bills or scanning lists, the tracking ledger simply loops over an array of vehicles and calls .calculate_rental_cost(3). The system never checks the car type. The individual objects intelligently compute their own distinct mathematics at runtime based on their specific subclass rules.
4. Encapsulation
Data is highly sensitive. If any part of our program could casually run system.revenue = 10000000, the financial integrity of SafariDrive Rentals would collapse.
Encapsulation locks critical states down using private variables (__). They can only be altered safely through validated system methods:
class CarRentalSystem:
def __init__(self, name="SafariDrive Rentals"):
self.vehicles = []
self.__revenue = 0.0 # Private attribute: completely shielded
@property
def revenue(self):
return self.__revenue # Read-only access exposed safely to the outside
def process_payment(self, payment):
if payment.process_payment(): # Updates ONLY after a verified financial success
self.__revenue += payment.amount
return True
return False
Tracing the Complete Lifecycle Engine
RentFlow functions using a fairly strict, workflow lifecycle:
Register Customer -> Search Inventory -> Rent Vehicle (Marks Unavailable) -> Return Vehicle (Assesses Fees & Frees Asset) -> Process Payment -> Update Business Revenue
Programmatic Late-Return Fee Calculation
When a car is returned late, the transactional contract manager (Rental) calculates the penalty details using the daily rate alone, ignoring baseline luxury or service add-ons:
[\text{Late Days} = \text{Actual Days} - \text{Agreed Rental Days}]
[\text{Daily Late Penalty} = \text{Vehicle Daily Rate} \times 20\%]
[\text{Late Fee} = \text{Daily Late Penalty} \times \text{Late Days}]
If a customer rents a Toyota Prado (KSh 8,000/day) for 4 days but returns it after 6 days, the manager flags it as 2 days late. It appends a dynamic late fee of KSh 3,200 seamlessly onto a single, finalized invoice statement before letting the customer approach the payment processors.
Building an Enterprise User Interface
In the operational control script (main.py), every interactive menu option is wrapped defensively inside a comprehensive execution handler:
try:
choice = input("Choose an option: ").strip()
if choice == "4":
# ... logic to trigger system.rent_vehicle() ...
except Exception as error:
print(f"\n[SYSTEM BLOCKAGE] Error: {error}")
If a clerk enters negative rental days, tries to check out a vehicle that is already actively leased on another contract, or creates a profile without a valid driver's license, RentFlow safely catches the custom raised ValueError. It outputs an alert message directly to the screen and smoothly drops the user back to the primary menu rather than letting the underlying app crash.
What I Learned
By building this project, I walked away with critical real-world engineering insights:
-
Clean code architecture pays off early. Splitting files made debugging easier. When payment logic had an issue, I knew where to look inside
payment.pywithout digging through hundreds of UI lines. - Defensive programming saves business relationships. Throwing errors immediately inside constructor setups prevents invalid data corruptions from breaking state tracking deeper down the pipeline.
Have you ever had to rescue a business or personal workflow from a giant, breaking spreadsheet? Let me know in the comments how you approached designing your solution!
Top comments (0)