Liskov Substitution
TL;DR: Subclasses must work wherever the base class works. If substituting a subclass for its parent breaks the program — throws unexpected exceptions, ignores method calls, or requires callers to add type checks — the hierarchy is wrong. Fix the abstraction, not the caller.

Why This Matters in Interviews
LSP violations are one of the most common design mistakes in LLD interviews. A candidate models the real world ("a penguin IS a bird") without thinking about whether the subclass actually fulfills the base class's contract. The result is fragile code full of instanceof checks and try/catch blocks that exist only to work around a bad hierarchy.
Interviewers test LSP to see if you can identify when inheritance is being misused and restructure the hierarchy correctly.
The Problem: Penguin Is a Bird... But Can't Fly
// BAD: Penguin inherits fly() but can't actually fly
class Bird {
public void fly() {
System.out.println("Flying through the air");
}
public void eat() {
System.out.println("Eating food");
}
}
class Sparrow extends Bird {
// fly() works fine — sparrows can fly
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
// BAD: Same problem in C++
class Bird {
public:
virtual ~Bird() = default;
virtual void fly() { std::cout << "Flying through the air" << std::endl; }
virtual void eat() { std::cout << "Eating food" << std::endl; }
};
class Sparrow : public Bird {
// fly() works fine
};
class Penguin : public Bird {
public:
void fly() override {
throw std::logic_error("Penguins can't fly!");
}
};
# BAD: Same problem in Python
class Bird:
def fly(self) -> None:
print("Flying through the air")
def eat(self) -> None:
print("Eating food")
class Sparrow(Bird):
pass # fly() works fine
class Penguin(Bird):
def fly(self) -> None:
raise NotImplementedError("Penguins can't fly!")
What Breaks
Any code that works with Bird objects now has a hidden landmine:
// This code SHOULD work for any Bird — but it doesn't
void migrateBirds(List<Bird> flock) {
for (Bird bird : flock) {
bird.fly(); // BOOM — crashes if any bird is a Penguin
}
}
The caller trusted the Bird contract: "all birds can fly." Penguin broke that contract. Now every caller has to defend against the possibility that fly() might explode:
// BAD: Callers patching around a broken hierarchy
void migrateBirds(List<Bird> flock) {
for (Bird bird : flock) {
if (bird instanceof Penguin) { // RED FLAG
System.out.println("Skipping penguin");
} else {
bird.fly();
}
}
}
Interview tip: The moment you see
instanceofchecks in code that should work polymorphically, you're looking at an LSP violation. The fix is never "add more type checks." The fix is restructure the hierarchy.
The Fix: Separate What Can Fly From What Can't
// GOOD: Birds don't promise flight. Only FlyingBirds do.
class Bird {
public void eat() {
System.out.println("Eating food");
}
}
class FlyingBird extends Bird {
public void fly() {
System.out.println("Flying through the air");
}
}
class Sparrow extends FlyingBird {
// Inherits both eat() and fly() — both work correctly
}
class Penguin extends Bird {
// Inherits eat() — no fly() method to violate
public void swim() {
System.out.println("Swimming through the water");
}
}
// GOOD: Same fix in C++
class Bird {
public:
virtual ~Bird() = default;
virtual void eat() { std::cout << "Eating food" << std::endl; }
};
class FlyingBird : public Bird {
public:
virtual void fly() { std::cout << "Flying through the air" << std::endl; }
};
class Sparrow : public FlyingBird {
// Inherits both eat() and fly()
};
class Penguin : public Bird {
public:
void swim() { std::cout << "Swimming through the water" << std::endl; }
};
# GOOD: Same fix in Python
class Bird:
def eat(self) -> None:
print("Eating food")
class FlyingBird(Bird):
def fly(self) -> None:
print("Flying through the air")
class Sparrow(FlyingBird):
pass # Inherits both eat() and fly()
class Penguin(Bird):
def swim(self) -> None:
print("Swimming through the water")
Now migrateBirds takes List<FlyingBird> instead of List<Bird>, and every bird in that list is guaranteed to fly. No instanceof checks. No surprise exceptions. The type system enforces the contract.
// GOOD: The type signature guarantees safety
void migrateBirds(List<FlyingBird> flock) {
for (FlyingBird bird : flock) {
bird.fly(); // Always works — the type guarantees it
}
}
Red Flags: How to Spot LSP Violations
Watch for these in your own designs and in code review:
| Red Flag | What it means |
|---|---|
Subclass throws UnsupportedOperationException for an inherited method |
The subclass doesn't fulfill the base class contract |
Callers use instanceof to decide what to do |
The hierarchy forces callers to know about specific subtypes |
| Subclass overrides a method to do nothing (empty body) | The subclass is pretending to support behavior it doesn't have |
| Subclass strengthens preconditions ("only works if X") | Callers that worked with the base class will break with the subclass |
Interview tip: When you're designing a class hierarchy, ask yourself: "Can I use any subclass in place of the parent without the caller knowing?" If the answer is no, restructure the hierarchy. Don't patch it with type checks.
Quick Recap
| Concept | What it means | Why it matters |
|---|---|---|
| Substitutability | Subclasses must work anywhere the base class works | Callers shouldn't need to know which subtype they have |
| Contract preservation | Subclasses must honor all promises made by the base class | Throwing exceptions for inherited methods breaks the contract |
| Fix the hierarchy | If a subclass can't fulfill the contract, the inheritance structure is wrong | Restructure the classes instead of adding instanceof checks |
| Red flags | Empty overrides, UnsupportedOperationException, instanceof guards | All signs that the abstraction needs rethinking |