Inheritance and Generalization

IS-A relationships, when to reach for them, and the cost of getting it wrong. Composition as the more common right answer.

Concept Foundational
10 min read
oop inheritance generalization composition lsp

Summary#

Inheritance is the language-level way to say “a Car is a kind of Vehicle — the child inherits the parent’s fields and methods, can extend them, and can refine some by overriding. Generalization is the conceptual move that justifies the code: noticing that two or more classes share a real, named, abstract concept and lifting that concept up to be a type of its own.

In one line: inheritance is IS-A, made executable. Said badly, it is the most expensive form of code reuse there is — a permanent coupling between parent and child that you cannot back out of without changing every caller that holds the parent type. Said well, it is the cleanest way to name a hierarchy of types whose differences live in what they do, not in what they have.

The honest summary every LLD interview expects you to hold: prefer composition; reach for inheritance only when you have a genuine generalization to name, and even then keep the hierarchy shallow. The first two words of “composition over inheritance” do the heavy lifting; the rest is commentary.

Why it matters#

Inheritance is the pillar most often misused, and the misuse compounds across a design. A wrong choice of base class makes every subclass a little bit wrong, every override a little bit forced, and every Liskov check a little bit anxious. The reviewer in an LLD interview has seen the mistakes; the moment you draw a deep chain or pick a base class for the wrong reason, the next twenty minutes are spent recovering.

Three concrete costs of getting inheritance wrong:

Permanent coupling. A subclass depends on its parent’s implementation, not just its interface — any change to a protected method, a constructor parameter, or even a private invariant the parent assumed can break a subclass that the parent’s author never knew about. The fragile base class problem is the formal name; in interview it shows up as “if I change this in Vehicle, what breaks in Truck?”

Orthogonal axes collapsed into one tree. A Vehicle that wants to vary by body type (Car, Truck, Motorcycle) and by fuel type (Petrol, Diesel, Electric) cannot represent both in a single inheritance tree without producing six subclasses for the cartesian product. Composition handles both axes naturally: Vehicle has a BodyType and a FuelType, each picked independently.

Liskov violations. Asserting Square extends Rectangle looks fine until a caller holding a Rectangle calls setWidth(5) and discovers the height changed too. The hierarchy is unsound; the only fixes are to weaken the parent’s contract or to rebuild the relationship. Both cost more than picking composition the first time.

The strongest signal you can send on this pillar: when the variation is “different kinds of the same thing, all honouring the same contract”, you reach for inheritance; when the variation is “the same kind of thing, varying along independent axes”, you reach for composition. Confusing the two is the most common LLD mistake at the foundations level.

How it works#

The mechanical move is to find the shared abstract concept, name it as a class or interface, and let the specialised variants extend or implement it. The discipline is to do this only when the shared concept is real — when removing it would force callers to handle the variants individually.

A worked example from a Parking Lot design, where the variation is “different kinds of vehicle, each with its own size class and fee.”

public abstract class Vehicle {
protected final String licensePlate;
protected final VehicleSize size;
protected Vehicle(String licensePlate, VehicleSize size) {
this.licensePlate = Objects.requireNonNull(licensePlate);
this.size = Objects.requireNonNull(size);
}
public String licensePlate() { return licensePlate; }
public VehicleSize size() { return size; }
public abstract Money parkingFeePerHour();
public boolean fitsIn(Spot spot) {
return spot.size().accommodates(this.size);
}
}
public final class Motorcycle extends Vehicle {
public Motorcycle(String licensePlate) {
super(licensePlate, VehicleSize.SMALL);
}
@Override public Money parkingFeePerHour() { return Money.rupees(20); }
}
public final class Car extends Vehicle {
public Car(String licensePlate) {
super(licensePlate, VehicleSize.MEDIUM);
}
@Override public Money parkingFeePerHour() { return Money.rupees(50); }
}
public final class Truck extends Vehicle {
public Truck(String licensePlate) {
super(licensePlate, VehicleSize.LARGE);
}
@Override public Money parkingFeePerHour() { return Money.rupees(150); }
}

Read the hierarchy and ask three questions:

Is the IS-A relationship real? A Motorcycle is, in the domain of parking, genuinely a kind of Vehicle. The parking lot does not care that one has two wheels; it cares that it occupies a small spot and pays a per-hour fee. Yes, real.

Does every subclass honour the parent’s contract? parkingFeePerHour() is abstract, so every subclass must provide one. fitsIn(Spot) is concrete, defined once on the parent in terms of size(). Every subclass passes the Liskov check trivially — none of them weakens a precondition or strengthens a postcondition.

Is the hierarchy shallow? One level. The reviewer will not complain.

Contrast with a wrong shape. Suppose you also want to vary by fuel type: petrol, diesel, electric. The naive move is ElectricCar extends Car. The cost: now you have a parallel tree for every fuel type, and a DieselTruck is six inheritance hops from Vehicle. The right move is composition:

public abstract class Vehicle {
protected final String licensePlate;
protected final VehicleSize size;
protected final FuelType fuel;
protected Vehicle(String licensePlate, VehicleSize size, FuelType fuel) {
this.licensePlate = licensePlate;
this.size = size;
this.fuel = fuel;
}
public abstract Money parkingFeePerHour();
public Money chargingFeePerHour() {
return fuel.chargingFeePerHour(); // delegated to the FuelType
}
}
public enum FuelType {
PETROL { public Money chargingFeePerHour() { return Money.ZERO; } },
DIESEL { public Money chargingFeePerHour() { return Money.ZERO; } },
ELECTRIC{ public Money chargingFeePerHour() { return Money.rupees(40); } };
public abstract Money chargingFeePerHour();
}

Vehicle inherits downwards along the body-type axis (Car / Motorcycle / Truck) and composes along the fuel-type axis. Each axis sits where it serves the model best, and the class count stays linear in the number of variants instead of quadratic.

Variants and trade-offs#

Several distinct shapes hide behind the word “inheritance”; naming them lets you choose deliberately.

Interface inheritance — implements. A class adopts a contract. No state, no implementation, just the promise to provide the listed methods. This is the form to reach for first; it is the least coupling for the most type-level expression. Default to it.

Abstract class inheritance — extends with abstract. A class adopts a contract and inherits partial implementation. Useful when several subclasses share genuine skeleton code — a retry wrapper, a logging hook, a stable algorithm with one variable step (this is the Template Method pattern in pattern vocabulary).

Concrete class inheritance — extends with no abstract. A class extends another class that is itself instantiable. The most fraught shape: changes to the parent now ripple in two directions (to callers and to subclasses). Defensible when you really do have a parent type with full behaviour and a child that genuinely specialises it; in interview, the reviewer will usually nudge you toward composition.

A. Inheritance fits. Three classes share an abstract concept (Vehicle), the shared concept has a clear contract (parkingFeePerHour, fitsIn), and adding a new kind means writing one new class. Depth ≤ 2. Every subclass passes Liskov trivially.

B. Composition fits. The shared “concept” is really a bag of independent traits — fuel type, body type, ownership status, payment plan. Inheritance would multiply the class count; composition keeps each trait a field whose type is an interface or enum, picked independently.

The trade-off worth naming: inheritance gives you the cleanest polymorphic dispatch, because the parent type is the natural place to hold the abstract method. Composition gives you the cleanest independent variation, because each axis can change without touching the others. The choice is which property matters more for the variation you actually have.

A second trade-off: depth. A two-level hierarchy is easy to reason about; a five-level one is not. The base-class author can no longer predict what the leaves assume, and the leaves cannot easily tell which ancestor a behaviour came from. The rule of thumb: depth ≤ 2 in interview answers. Past that, refactor to composition or to strategy objects.

A third: final matters. Marking a method or class final is an explicit statement that this is not an extension point — that subclasses must not override here. Without it, every subclass can quietly change behaviour the parent depended on. In well-designed inheritance hierarchies, final appears liberally on methods the parent considers part of its own contract.

When this is asked in interviews#

Inheritance is the pillar where the reviewer is most likely to set a trap. Three forms come up consistently.

The IS-A challenge. You draw Manager extends Employee. The reviewer asks: “Can a Manager be promoted to a Director? Can an Employee become a Manager later?” If yes — and almost always the answer in real systems is yes — then the relationship is not IS-A; it is a role an Employee can play. The right model is Employee with a Role field, not a separate subclass per role. Catching this before the follow-up is a strong signal.

The Liskov check. You propose Square extends Rectangle. The reviewer asks: “What does setWidth(5) do if I’m holding a Square typed as Rectangle?” The answer — height also changes — violates the parent’s implicit contract that width and height are independent. The deeper lesson the reviewer wants you to articulate: a subclass must be substitutable for its parent everywhere the parent is used; if it can’t, the inheritance is wrong.

The orthogonal-axes question. You propose ElectricCar extends Car extends Vehicle. The reviewer asks: “How would you model a DieselTruck?” If the answer requires inventing a new subclass for every combination, the model is wrong. The reviewer is fishing for “I’d pull fuel type out as a field” — the recognition that you have two independent axes of variation and that inheritance handles only one of them well.

The “why not composition?” question. A direct prompt sometimes, an implicit one always. The honest answer in interview is “I’d use composition by default; I reached for inheritance here because [name the shared abstract concept].” If you cannot name the shared abstract concept, the reviewer will ask you to back out the inheritance — saving them the question by composing in the first place is the cleaner outcome.

The strongest signal you can send on this pillar: when the reviewer adds a new requirement that introduces a new axis of variation, your model absorbs it as a field, not as a new subclass. That is composition working as the default, with inheritance reserved for the cases where it actually earns its keep.

Inheritance is the pillar most tightly coupled to the others — abstraction supplies the shape of the parent contract, polymorphism is the runtime payoff, and encapsulation is what keeps the parent’s invariants safe across subclasses.

  • OOP Fundamentals — The Four Pillars — the four pillars in one page, inheritance among them.
  • Encapsulation — a subclass can only honour the parent’s contract because the parent’s invariants are enforced behind methods.
  • Abstraction — abstract classes and interfaces are the language-level shapes inheritance plugs into.
  • Polymorphism — the property inheritance was introduced to enable; calling code that doesn’t care which subclass is in play.
  • Liskov Substitution Principle (LSP) — inheritance’s contract, said carefully; if your hierarchy violates it, the hierarchy is wrong.
  • Open Closed Principle (OCP) — inheritance plus polymorphism is one of the two clean ways to add behaviour without touching existing code (composition with strategies is the other).
  • Template Method Pattern — the canonical “abstract class with a fixed skeleton and variable steps” use of inheritance.
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.