Skip to content

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.

Liskov Substitution Principle Violation

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 instanceof checks 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