skip to content

Liskov Substitution Principle

A subtype must be usable anywhere its parent is, which is a statement about behavior and not just about method signatures. You will work through the classic Rectangle/Square and throw-on-override violations and learn why they only bite at runtime.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

What is the Liskov Substitution Principle (the "L" in SOLID), and what does it require of a subtype?

level: juniorimportance: must knowfreq 85%

answer

  1. Subtype substitutable for supertype
  2. Behaviour, not just signature
  3. Preconditions ≤, postconditions ≥, invariants kept
  4. No throw-on-override, no instanceof in clients
  5. Liberal in accepting, conservative in promising

basics

~20 s

If type S is a subtype of type T, code written against T must keep working correctly when handed an S. A subtype must honour the promises its supertype made — not just match method names.

solid answer

~50 s

LSP says: objects of a subtype must be usable anywhere the supertype is expected, without the calling code needing to know the difference or breaking. Barbara Liskov's formulation is about *behavioural* subtyping: matching the compiler-checked signature is necessary but not sufficient. The subtype must also accept at least everything the supertype accepted (preconditions not strengthened), guarantee at least everything the supertype guaranteed (postconditions not weakened), and keep every invariant the supertype maintained. Practical smells that a subtype violates LSP: overriding a method to throw "not supported", requiring extra setup the base type never required, returning null or a narrower/weirder result where the base promised a value, or callers doing type checks (`if (x is Square) ...`) to work around a subtype. The fix is usually to stop inheriting: extract a common abstraction, compose, or split the interface.

go deeper

for a junior

State the substitutability definition and give one concrete broken example (an override that throws "not supported"). Say it is about behaviour, not just matching method signatures.

for a middle

Add the three Design-by-Contract rules — preconditions not strengthened, postconditions not weakened, invariants preserved — and name the smells (throw-on-override, instanceof in clients, extra required setup).

for a senior

Connect the rules to language variance (covariant returns, contravariant parameters, no broader exceptions), and to fixes: composition, interface segregation, immutability, contract tests shared across implementations.

for a principal

Frame LSP as contract governance beyond a single codebase: it is the same rule behind backward-compatible API/event-schema evolution and plugin ecosystems. Discuss deciding contract breadth up front, the history rule, and using consumer-driven contract tests as the enforcement mechanism.

## The principle The Liskov Substitution Principle (LSP) is the **L** in SOLID. It comes from Barbara Liskov and Jeannette Wing's work on *behavioural subtyping*. The usual phrasing: > 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 (correctness, task performed, etc.). Plainly: **any code written against the general type must still be correct when it is secretly handed a specialised type.** ### Terms defined - **Supertype / base type**: the general type — an interface, abstract class, protocol, or trait — that client code is written against (e.g. `Account`, `Shape`, `Collection`). - **Subtype**: a type declared to be usable in place of the supertype — via class inheritance, interface implementation, or structural conformance. - **Client code**: any code that holds a reference typed as the supertype and calls methods on it. Crucially, the client was written and tested **knowing only the supertype's documented behaviour**. - **Substitutability**: the property that swapping in a subtype instance changes nothing the client can observe as a *violation of the contract* (it may change performance or which concrete algorithm runs — that's fine). ### Why signatures are not enough Most languages check only the *syntactic* part: does the subtype have a method with a compatible name and parameter/return types? A compiler will happily accept a subtype that is behaviourally nonsense: - `Square extends Rectangle` where setting the width silently also changes the height — compiles, breaks any client that sets width and height independently. - `ImmutableList implements List` where `add()` throws `UnsupportedOperationException` — compiles, breaks any client that was told `List` supports `add`. - `FastCache extends Cache` where `get()` requires `connect()` to have been called first — compiles, breaks clients that never heard of `connect()`. LSP is about the **contract**, the behaviour promised in documentation, tests, and the type's semantics — not the method table. ### The three rules (Design by Contract framing) Bertrand Meyer's Design by Contract gives the operational checklist. For each overridden operation: 1. **Preconditions may not be strengthened.** A precondition is what the caller must guarantee before calling. If `Base.withdraw(amount)` accepts any positive amount, a subtype that additionally demands `amount <= 100` will reject calls the client was told were legal. Subtypes may *weaken* (accept more) — that never breaks an existing caller. 2. **Postconditions may not be weakened.** A postcondition is what the method guarantees on return. If `Base.find(id)` promises a non-null result or an exception, a subtype returning `null` breaks clients. Subtypes may *strengthen* (promise more) — e.g. also promise the result is sorted. 3. **Invariants must be preserved.** An invariant is a property true of the object before and after every public operation (`balance >= 0`; `width` and `height` are independent). A subtype may add invariants of its own, but may not break the base's. Liskov and Wing add a **history rule**: the subtype must not allow state changes the supertype's clients believe impossible — e.g. an "immutable point" base type subclassed by a mutable point. A memorable summary: **be liberal in what you accept, conservative in what you promise.** ### Signature variance (the type-system half) Languages encode part of the rule in their variance rules for overriding: - **Return types are covariant**: an override may return a *narrower* type (`Animal` → `Dog`). That is postcondition strengthening — safe. - **Parameter types should be contravariant**: an override could safely accept a *wider* type (`Dog` → `Animal`). That is precondition weakening — safe. Most mainstream OO languages (Java, C#, Kotlin, C++) don't actually support contravariant parameters; they require invariant parameter types, and a differing parameter type just creates an overload, not an override. - **Exceptions are covariant**: an override may throw fewer or narrower checked exceptions, never new broader ones — throwing something the client was never told about is postcondition weakening. Variance handles only the parts the compiler can see. Preconditions/postconditions/invariants expressed in *values* ("amount must be positive", "never returns empty") remain the developer's responsibility. ### How violations show up in real code - `throw new UnsupportedOperationException()` in an override. - The client doing `instanceof` / `is` / `typeof` checks and branching per subtype. - Base-class tests that fail when re-run against a subclass. - Extra required call order ("you must call `init()` first on this one"). - Configuration flags on the base whose meaning changes per subclass. ### How to fix - **Extract the true common abstraction**: if only some shapes can resize independently, `Shape` shouldn't have `setWidth`/`setHeight`; put them on `Rectangle` only. - **Prefer composition over inheritance**: `Square` *has a* side length and can expose `area()`, without pretending to be a `Rectangle`. - **Split the interface** (Interface Segregation): `ReadableList` vs `MutableList`, so immutability isn't a broken promise. - **Make objects immutable**: with no setters, the Rectangle/Square conflict evaporates because there is no mutation to be inconsistent about. - **Verify with contract tests**: one test suite written against the supertype's contract, executed against every implementation. ### Trade-offs and nuance - LSP is about *observable contract*, not perfect equivalence. Different performance, different logging, different persistence backend — all fine. - The contract is whatever clients were *told*. If the base type documents "implementations may reject amounts over their configured limit", a limiting subtype is compliant; the precondition was always that broad. Much of "LSP compliance" is therefore a documentation decision made up front. - Strict LSP is expensive to enforce in dynamic or plugin ecosystems; contract tests are the pragmatic substitute for formal proofs.

  • Does LSP apply only to class inheritance?
    No. It applies to any subtyping relation clients rely on: interface implementations, duck-typed objects in dynamic languages, protocol conformance, and even a new version of a remote API replacing the old one behind the same URL. Anywhere a caller was written against contract A and receives implementation B, LSP is the question.
  • How would you detect an LSP violation before it reaches production?
    Write contract tests: a shared test suite expressed purely in terms of the supertype's promises, parameterised over every implementation. Any subtype that can't pass it is not substitutable. Additionally, code review for `UnsupportedOperationException` in overrides and for type checks in client code.

A rental car contract says: any car you're given starts with the key, seats four, and drives on the motorway. Substituting a sportier model is fine. Substituting a go-kart that has four seats painted on but can't legally reach motorway speed breaks every plan the renter made — even though it is technically "a vehicle with four seats".

context

open as a page

State the Design-by-Contract rules a subtype must obey to satisfy the Liskov Substitution Principle — preconditions, postconditions, invariants — and give an example of breaking each one.

level: middleimportance: must knowfreq 65%

basics

~20 s

A subtype may not demand more than the base (preconditions can only be weakened), must promise at least as much as the base (postconditions can only be strengthened), and must keep every rule the base always held true (invariants preserved).

open as a page

Explain the classic Rectangle/Square problem: why does making Square a subclass of a mutable Rectangle violate the Liskov Substitution Principle, and how would you fix the design?

level: middleimportance: must knowfreq 75%

basics

~20 s

A mutable Rectangle lets you set width and height independently. A Square must keep them equal, so its setters change both. Code that sets width 5, height 4 and expects area 20 gets 16 — the subclass broke a rule the base promised.

open as a page

An interface has an `add(item)` method, and one implementation overrides it to throw an "operation not supported" error (as immutable collections often do). Is that a Liskov Substitution violation, and what are the alternatives?

level: seniorimportance: should knowfreq 55%

basics

~10 s

Yes — clients told the interface supports add now crash at runtime. The implementation delivers less than the interface promised. The fix is to split the interface so read-only types never advertise mutation.

open as a page

How do covariance and contravariance in method signatures relate to the Liskov Substitution Principle, and what do mainstream languages actually enforce?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Overrides may return a more specific type (covariant returns) because promising more is safe, and could in theory accept a more general parameter type (contravariant parameters) because demanding less is safe. Most languages enforce only the return-type rule.

open as a page

Beyond class hierarchies, how does the Liskov Substitution Principle apply to service APIs, message schemas, and plugin implementations — and how would you enforce substitutability across teams?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Any time a consumer written against one contract receives a different implementation or version, LSP applies: a new version must not demand more of callers or promise less. Enforce it with shared contract tests run in every provider's pipeline.

open as a page