← Home
🏗Crack System Design with DSA AnimatorSystem DesignNEW
🏗 System Design›SOLID Principles

🔁Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types — without surprising callers.

📌 Key IdeaIf S is a subtype of T, then objects of type T may be replaced with objects of type S without breaking any desirable property of the program (correctness, expectations).
✨

Why It Matters

  • Polymorphism actually works — Code that accepts Reader must not special-case BadReader — or abstraction is a lie.
  • Fewer `instanceof` hacks — If you need if (x instanceof Square) everywhere, LSP is already broken.
  • Safer libraries — Frameworks assume substitutability — break LSP and subtle bugs appear only in production.
💡

In Layman's Terms

If you rent a car and the agency hands you a substitute vehicle, it must still drive like a car — steering wheel, pedals, fuel cap in the usual place. If the substitute is a submarine shaped like a car, something is wrong with your type hierarchy.

LSP says: inheritance is not "IS-A in the real world" — it is "honours the contract of the parent for every method a caller might invoke."

🌍

Real-World Analogy

🎭 Stand-in actor

A theatre understudy must perform the same script cues as the lead — not improvise a different ending because they prefer it.

Your subclass is the understudy: same observable behaviour from the caller's point of view (preconditions, postconditions, invariants).

🖼

Visual

✅ Subtype preserves the contract
Caller sets width and height independently — area should stay predictable.
  void reportArea(Rectangle r) {
      r.setWidth(5);
      r.setHeight(4);
      assert area == 20;   // true for Rectangle
  }

  reportArea(new Rectangle());   // OK
  reportArea(new Square());      // ??? Square must not break expectations
❌ Classic LSP break
Square “is-a” Rectangle in geometry, but coupled width/height breaks callers who expect independent sides.
  class Square extends Rectangle {
      void setWidth(int w) { super.setWidth(w); super.setHeight(w); }
      void setHeight(int h){ super.setHeight(h); super.setWidth(h); }
  }
  // Caller thinks width and height are independent — Square couples them.

Rectangle vs Square is the textbook example: geometric "is-a" ≠ behavioural substitutability.

✅

When to Use

  • Whenever you use inheritance or implementation of an interface — ask: can I swap implementations blindly?
  • When you design API contracts — document pre/post conditions and enforce them in subclasses.
  • When reviewing PRs that add a new implementation of an existing type.
🚫

When NOT to Use

  • You are using composition only — LSP applies to inheritance/subtyping, not every delegation.
  • Value objects with no polymorphic clients — less relevant than deep class hierarchies.
🚨

Anti-Pattern (What NOT to Do)

Square extends Rectangle — breaks independence of width/heightBadjava
class Rectangle {
    protected int w, h;
    public void setWidth(int w)  { this.w = w; }
    public void setHeight(int h) { this.h = h; }
    public int area() { return w * h; }
}

class Square extends Rectangle {
    @Override public void setWidth(int w)  { super.setWidth(w);  super.setHeight(w); }
    @Override public void setHeight(int h) { super.setHeight(h); super.setWidth(h); }
}

// Caller using Rectangle contract:
void resizeTo(Rectangle r, int w, int h) {
    r.setWidth(w);
    r.setHeight(h);
    // Expected area = w * h for independent dimensions — fails for Square.
Geometrically a square is a rectangle; behaviourally the contract "set width and height independently" is violated.
✅

The Right Way

Separate types or shape hierarchy without lyingGoodjava
interface Shape { int area(); }

record Rectangle(int width, int height) implements Shape {
    public int area() { return width * height; }
}

record Square(int side) implements Shape {
    public int area() { return side * side; }
}

// No fake inheritance — each type models its own invariants clearly.
Prefer composition or parallel hierarchies over forcing IS-A when contracts diverge.
⚖️

Pros & Cons

👍 Pros
  • Polymorphic code stays simple — no special cases.
  • Aligns with Design by Contract (preconditions, postconditions).
  • Makes testing straightforward: one suite per abstraction.
👎 Cons
  • Strict LSP can push you toward composition — sometimes more boilerplate than naive inheritance.
  • Teams disagree on what the "contract" is — document it.
📊

Good subtype vs bad subtype

AspectHonours contract ✅Surprises callers 🚨
PreconditionsNot stricter than parentRejects inputs parent accepted
PostconditionsAt least as strongWeaker guarantees / wrong results
InvariantsPreserves parent invariantsNew state parent forbids
ExceptionsSame or narrowerNew checked exceptions parent didn't
🔀

Common Confusions Cleared Up

LSP vs "duck typing"

Duck typing substitutes by behaviour at runtime without a declared supertype. LSP is the OO contract version: the supertype's expectations must hold for all subtypes that claim to be substitutable.

LSP vs SRP

SRP asks one reason to change per class. LSP asks subtypes don't weaken parent guarantees. A class can be SRP-clean but LSP-broken (Square/Rectangle).

⚠️

Common Mistakes / Anti-Patterns

  • Empty overrides that throw UnsupportedOperationException — interface segregation often fixes this instead.
  • Subclass narrows return type incorrectly or strengthens preconditions beyond what callers expect.
  • Using inheritance only for code reuse (extends Utility) — no real IS-A relationship.
🎤

Interview Q&A

🔮

Likely Follow-up Questions

  • How does covariance/contravariance relate to LSP in Java generics?
  • When is Template Method safe for LSP vs when does it trap subclasses?
  • Explain Design by Contract in two sentences.
🧾

Quick Cheat Sheet

Core ruleSubtype must work anywhere the supertype is expected.
Smellinstanceof before calling subclass-specific behaviour.
FixComposition · split types · narrower interfaces.
ExampleSquare ≠ behavioural subtype of mutable Rectangle.
🔗

Related Topics