Liskov Substitution Principle
A subtype must be fully substitutable for its parent — code that works with the base type must work correctly with any derived type, without surprises.
★★★★★4/5Inside a codebase — classes, modules, files
How it works
Barbara Liskov introduced this principle in a 1987 conference keynote. Formally: if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.
In practice: a subclass must honour the behavioural contract of its parent. It can't throw exceptions the parent doesn't throw, can't have weaker preconditions than the parent requires, and can't have stronger postconditions than the parent guarantees.
The classic violation is the Square/Rectangle problem. Mathematically, a square IS-A rectangle. But if Rectangle has setWidth and setHeight, and Square overrides both to always set both dimensions equally, then code that creates a Rectangle and changes only the width gets a wrong area calculation — LSP is violated.
LSP applies to interfaces too, not just inheritance. Any implementation of an interface must behave as callers expect from the contract — not just satisfy the method signatures.
Implementation
TypeScript · Go · Rust// ❌ Square violates the Rectangle contract
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(w: number) { this.width = w; }
setHeight(h: number) { this.height = h; }
area() { return this.width * this.height; }
}
class Square extends Rectangle {
// ❌ Breaking the contract: setWidth changes height too
setWidth(s: number) { this.width = this.height = s; }
setHeight(s: number) { this.width = this.height = s; }
}
function assertArea(r: Rectangle) {
r.setWidth(4);
r.setHeight(5);
console.assert(r.area() === 20); // ❌ fails for Square — returns 25
}
// ✓ Model correctly — no broken hierarchy
interface Shape { area(): number; }
class Rectangle implements Shape {
constructor(private w: number, private h: number) {}
area() { return this.w * this.h; }
}
class Square implements Shape {
constructor(private side: number) {}
area() { return this.side ** 2; }
}
// Both are substitutable for Shape — neither breaks the other's contractWhy it matters
Violated LSP means you can't trust polymorphism. If some subclasses work but others silently behave differently, you're forced to add type checks — which defeats the purpose of polymorphism and introduces the bugs OCP was meant to prevent.
✓ When to use
- →Whenever you use inheritance or implement an interface
- →When writing a new implementation of an existing interface
- →When overriding methods in a subclass
- →When designing abstract base classes for others to extend
✗ When NOT to use
- →LSP is not optional — it's a correctness property, not a preference
- →If you can't satisfy LSP, prefer composition over inheritance
Trade-offs
Polymorphism is trustworthy — any implementation can be substituted safely
Some mathematical IS-A relationships (Square/Rectangle) can't be modelled with correct inheritance
Eliminates defensive type-checks in calling code
Designing correct base class contracts upfront is hard — requires anticipating all subtype needs
Makes interfaces robust against new implementations
Violations are often subtle and only caught through careful testing
In production
Any List<T> implementation (ArrayList, LinkedList, CopyOnWriteArrayList) is substitutable — collections code never checks the concrete type
Trait objects (Box<dyn Trait>) rely on LSP — any type implementing the trait can be used in place of any other
A mock HTTP client used in tests must satisfy the same contract as the real one — same status codes, same error types
Industry adoption
Related principles
Polymorphism
Allow different types to be used interchangeably through a shared interface — so the calling code doesn't need to know which concrete type it's working with.
Interface / Contract
Define what a component must do without dictating how it does it — so implementations can vary freely while callers remain stable.
Composition over Inheritance
Build complex behaviour by combining small, focused objects rather than extending a class hierarchy — keeping coupling low and flexibility high.
Open/Closed Principle
Software entities should be open for extension but closed for modification — add new behaviour by writing new code, not by changing existing code.