System Design (LLD): Inheritance & Polymorphism in OOP
This chapter completes our deep dive into the four foundational pillars of Object-Oriented Programming (OOP). Having covered Abstraction and Encapsulation in the previous section, we now explore Inheritance and Polymorphism. Using real-world analogies, code evolutions, and architectural domain models, this guide details how these pillars model domain entities cleanly, minimize code redundancy, and enable extensible software architecture.
1. Demystifying Inheritance: The Parent-Child Mental Model
In the physical world, entities rarely exist in total isolation. They exhibit natural hierarchical relationships where specialized entities inherit traits from broader, generic categories.
Real-Life Analogy: Generic Car vs. Specialized Cars
Imagine a generic concept of a Car. Every car possesses foundational traits such as a
brand, amodel, an engine status (isEngineOn), and acurrentSpeed. Every car can also execute base behaviors likestartEngine()andstopEngine().However, in reality, we drive specific variations:
- Manual Car: Inherits all standard car features but introduces specialized components like a gear system (
currentGear) and specialized behaviors likeshiftGear().- Electric Car: Inherits all standard car features but replaces/complements them with battery management (
batteryLevel) and specialized behaviors likechargeBattery().
Inheritance allows us to capture this exact relationship in code. It creates a Parent-Child (or Base-Derived) relationship, allowing derived classes to automatically inherit state and behavior from a base class without rewriting boilerplate logic.
2. Access Modifiers & Modes of Inheritance
To implement inheritance effectively, we must understand how visibility rules transition from Parent to Child classes.
The protected Access Modifier
While public members are accessible everywhere and private members are accessible only within the defining class, the protected modifier acts as a bridge:
- Outside World:
protectedmembers behave likeprivatemembers (inaccessible). - Child Classes:
protectedmembers behave likepublicmembers (fully accessible).
Note: In real-world enterprise software,
protectedis used specifically when child classes require direct access to internal state variables without exposing those variables to the public API.
Modes of Inheritance (C++ Specific Context)
When inheriting from a base class, C++ allows specifiers for the inheritance relationship itself: public Car, protected Car, or private Car.
Industry Reality & Best Practice
⚠️ Industry Insight: In 99% of Low-Level Design (LLD) scenarios and production engineering across languages (Java, C#, Python, TypeScript),
publicinheritance is used exclusively. Non-public inheritance restricts subtyping, breaks the Liskov Substitution Principle (LSP), and prevents child objects from substituting base objects seamlessly.
3. Practical Inheritance Implementation
Below is a implementation demonstrating clean base-derived modeling in C++. Notice how child classes consume base methods without redeclaring them.
#include <iostream>
#include <string>
// Parent Class (Base)
class Car {
protected:
std::string brand;
std::string model;
bool isEngineOn;
int currentSpeed;
public:
Car(std::string b, std::string m)
: brand(b), model(m), isEngineOn(false), currentSpeed(0) {}
void startEngine() {
isEngineOn = true;
std::cout << brand << " " << model << ": Engine started.\n";
}
void stopEngine() {
isEngineOn = false;
currentSpeed = 0;
std::cout << brand << " " << model << ": Engine stopped.\n";
}
};
// Derived Class 1: ManualCar
class ManualCar : public Car {
private:
int currentGear;
public:
ManualCar(std::string b, std::string m)
: Car(b, m), currentGear(0) {}
void shiftGear(int gear) {
currentGear = gear;
std::cout << brand << " " << model << ": Shifted to gear " << currentGear << ".\n";
}
};
// Derived Class 2: ElectricCar
class ElectricCar : public Car {
private:
int batteryLevel;
public:
ElectricCar(std::string b, std::string m)
: Car(b, m), batteryLevel(100) {}
void chargeBattery() {
batteryLevel = 100;
std::cout << brand << " " << model << ": Battery fully charged to 100%.\n";
}
};
4. Demystifying Polymorphism: "One Interface, Many Forms"
Etymologically, Polymorphism originates from Greek:
Poly= ManyMorph= Forms
In object-oriented software, polymorphism represents the ability of different underlying objects to process the same request or behavior in unique, specialized ways.
Real-World Mental Models for Polymorphism
Model 1: The Animal Kingdom (Dynamic / Behavioral Polymorphism)
Consider an generic
Animalcategory containingDuck,Human, andTiger.If we trigger a common stimulus—
run():
- The
Duckwaddles/flaps.- The
Humanruns on two legs.- The
Tigersprints on four legs at high velocity.The stimulus (
run()) is uniform across the entity family, but the internal mechanics execution varies dynamically depending on the concrete instance type.
Model 2: The Human Context (Static / Parameterized Polymorphism)
Consider a single
Humanobject invoking therun()action under varying internal/external constraints:
run()(Default mode): Normal jogging pace.run(isDanger = true)(Emergency context): High-adrenaline sprint.The same entity alters its operational execution based on contextual parameters supplied during execution.
5. The Two Pillars of Polymorphism
In systems design and software development, polymorphism splits into two structural categories:
6. Dynamic Polymorphism (Runtime Polymorphism / Method Overriding)
Dynamic Polymorphism occurs when a child class redefines (overrides) a behavior inherited from a base class. The decision of which concrete implementation to invoke is deferred to runtime, depending on the precise type of the object currently instantiated.
Key Rules for Method Overriding
- The method signature (Name, Return Type, and Parameter Types/Count) must be 100% identical across Parent and Child classes.
- The base class method must be marked explicit with
virtual(in C++), enabling late runtime dynamic lookup tables (V-Tables).
#include <iostream>
class Car {
protected:
std::string brand;
int currentSpeed;
public:
Car(std::string b) : brand(b), currentSpeed(0) {}
// Abstract interface provided via virtual methods
virtual void accelerate() {
currentSpeed += 10;
std::cout << brand << " baseline accelerate: Speed = " << currentSpeed << " km/h\n";
}
virtual ~Car() = default;
};
class ManualCar : public Car {
public:
ManualCar(std::string b) : Car(b) {}
// Overriding base behavior
void accelerate() override {
currentSpeed += 20; // Faster pickup due to transmission gear ratio
std::cout << brand << " Manual accelerate: High torque! Speed = " << currentSpeed << " km/h\n";
}
};
class ElectricCar : public Car {
private:
int batteryLevel;
public:
ElectricCar(std::string b) : Car(b), batteryLevel(100) {}
// Overriding base behavior
void accelerate() override {
if (batteryLevel > 0) {
batteryLevel -= 2;
currentSpeed += 15;
std::cout << brand << " Electric accelerate: Instant torque! Speed = "
<< currentSpeed << " km/h (Battery: " << batteryLevel << "%)\n";
}
}
};
7. Static Polymorphism (Compile-Time Polymorphism / Method Overloading)
Static Polymorphism allows multiple methods within the same class scope to share the exact same method name, provided their parameter list signatures differ.
Key Rules for Method Overloading
- Methods must share the same name.
- Methods must differ in parameter count, parameter types, or parameter ordering.
- Differing only by return type is invalid and results in a compilation error.
- Method resolution occurs strictly at compile time.
#include <iostream>
class ManualCar {
private:
int currentSpeed;
public:
ManualCar() : currentSpeed(0) {}
// Overloaded Method 1: Default acceleration increment
void accelerate() {
currentSpeed += 20;
std::cout << "Accelerated by default amount. Current Speed: " << currentSpeed << " km/h\n";
}
// Overloaded Method 2: Custom acceleration increment based on foot pressure
void accelerate(int pushIntensity) {
currentSpeed += (pushIntensity * 5);
std::cout << "Accelerated via custom pedal push (" << pushIntensity
<< "). Current Speed: " << currentSpeed << " km/h\n";
}
};
8. Step-by-Step Vertical Execution Flow
The vertical flow chart below illustrates how execution shifts during runtime for dynamic polymorphic dynamic dispatch:
9. Tabular Comparison: Static vs. Dynamic Polymorphism
10. Unified Production Architecture: Integrating All 4 OOP Pillars
The C++ program below synthesizes Abstraction, Encapsulation, Inheritance, and Polymorphism into a single software model.
#include <iostream>
#include <string>
#include <vector>
// ==========================================
// 1. ABSTRACTION: Abstract Base Class
// ==========================================
class Vehicle {
protected:
// ==========================================
// 2. ENCAPSULATION: Protected State Variables
// ==========================================
std::string brand;
std::string model;
int currentSpeed;
public:
Vehicle(std::string b, std::string m)
: brand(b), model(m), currentSpeed(0) {}
virtual ~Vehicle() = default;
// Pure Virtual Function / Abstract Interface
virtual void startEngine() = 0;
// Virtual Methods for Dynamic Polymorphism
virtual void accelerate() {
currentSpeed += 10;
std::cout << brand << " " << model << " accelerates to " << currentSpeed << " km/h.\n";
}
// Overloaded Methods for Static Polymorphism
virtual void accelerate(int boost) {
currentSpeed += boost;
std::cout << brand << " " << model << " boosts by " << boost
<< " to " << currentSpeed << " km/h.\n";
}
};
// ==========================================
// 3. INHERITANCE: Derived Concrete Class 1
// ==========================================
class ManualCar : public Vehicle {
private:
int currentGear;
public:
ManualCar(std::string b, std::string m)
: Vehicle(b, m), currentGear(1) {}
void startEngine() override {
std::cout << "[ManualCar] " << brand << " " << model
<< ": Clutch depressed, key turned. Engine roaring.\n";
}
// ==========================================
// 4. DYNAMIC POLYMORPHISM: Method Overriding
// ==========================================
void accelerate() override {
currentSpeed += 25;
std::cout << "[ManualCar] " << brand << " " << model
<< " gear acceleration. Speed: " << currentSpeed << " km/h.\n";
}
};
// ==========================================
// 3. INHERITANCE: Derived Concrete Class 2
// ==========================================
class ElectricCar : public Vehicle {
private:
int batteryLevel;
public:
ElectricCar(std::string b, std::string m)
: Vehicle(b, m), batteryLevel(100) {}
void startEngine() override {
std::cout << "[ElectricCar] " << brand << " " << model
<< ": Power button pressed. Silent start, systems active.\n";
}
void accelerate() override {
if (batteryLevel > 5) {
batteryLevel -= 5;
currentSpeed += 20;
std::cout << "[ElectricCar] " << brand << " " << model
<< " silent instant torque. Speed: " << currentSpeed
<< " km/h (Battery: " << batteryLevel << "%).\n";
} else {
std::cout << "[ElectricCar] Low battery! Cannot accelerate.\n";
}
}
};
// ==========================================
// MAIN EXECUTION WORKFLOW
// ==========================================
int main() {
// Polymorphic collection using Base pointers
std::vector<Vehicle*> fleet;
fleet.push_back(new ManualCar("Suzuki", "Wagon R"));
fleet.push_back(new ElectricCar("Tesla", "Model S"));
std::cout << "--- STARTING FLEET OPERATIONS ---\n";
for (Vehicle* v : fleet) {
v->startEngine(); // Dynamic Polymorphism
v->accelerate(); // Dynamic Polymorphism (Default acceleration)
v->accelerate(30); // Static Polymorphism (Overloaded method invocation)
std::cout << "-----------------------------------\n";
}
// Cleanup resources
for (Vehicle* v : fleet) {
delete v;
}
return 0;
}
11. Key Pitfalls & Industry Realities
1. Breaking Inheritance Principles via Non-Public Modes
Inheriting private or protected base classes restricts downstream extension. If ManualCar inherits privately from Car, a specialized WagonR derived from ManualCar cannot access core Car behaviors. Always favor public inheritance unless implementing specialized private implementation details.
2. Deep Class Hierarchies (The Fragile Base Class Problem)
Extremely deep class hierarchies (e.g., Entity -> Vehicle -> MotorVehicle -> FourWheeler -> Car -> ManualCar -> WagonR) lead to tight coupling. Modifying a base variable in Vehicle risks breaking behavior five levels down in WagonR.
💡 Design Guideline: Prefer shallow inheritance trees (2-3 levels maximum). Utilize Composition over Inheritance where appropriate.
3. Missing Virtual Destructors
When dynamically allocating derived objects assigned to base pointer references (Vehicle* v = new ElectricCar()), failing to define a virtual ~Vehicle() = default; in the base class leads to undefined behavior and memory leaks because derived class destructors are never invoked.
12. Summary and Key Takeaways
- Inheritance Purpose: Captures natural Parent-Child (Is-A) relationships between real-world domain entities, allowing child classes to reuse state and behavior while introducing specialized extensions.
- Access Control: The
protectedaccess modifier exposes variables and behaviors exclusively to child classes while keeping them hidden from the outside world. - Public Inheritance Dominance: While language specifications support
protectedandprivateinheritance modes, production LLD exclusively usespublicinheritance to maintain structural subtyping and LSP compliance. - Polymorphism Core: Represents "One Interface, Many Forms"—enabling unified calls to adapt behavior based on contextual arguments or concrete underlying object types.
- Dynamic Polymorphism: Resolved at runtime using late binding (V-Tables) via Method Overriding (matching method signatures between parent and child).
- Static Polymorphism: Resolved at compile-time via Method Overloading (differing parameter signature counts or types within the same class context).
- Abstract Base Contracts: Combining
virtualmethods with pure abstract definitions guarantees that child classes implement mandatory custom behaviors (accelerate()) while inheriting foundational shared utilities (startEngine()). - Unified OOP Harmony: The four pillars operate in tandem: Encapsulation secures state inside classes, Abstraction hides implementation detail, Inheritance structures entity relationships, and Polymorphism allows polymorphic dynamic dispatch across uniform interfaces.