SOLID & DESIGN PRINCIPLES / OBJECT DESIGN BRIEF

Liskov Substitution Principle

The Liskov Substitution Principle (LSP) says objects of a subtype must be usable wherever the base type is expected without breaking the program.

IntermediatePhase 02 / Topic 3 of 8ResponsibilitiesCollaborationsExtensibility
01

Overview

The Liskov Substitution Principle (LSP) says objects of a subtype must be usable wherever the base type is expected without breaking the program. A subclass may extend behavior, but it must honor the parent's contract: it cannot demand more from callers (stronger preconditions) or promise less (weaker postconditions), and it must preserve the parent's invariants.

The classic violation is Square extends Rectangle: setting a square's width also changes its height, breaking code that assumes width and height are independent. LSP violations show up as instanceof checks, overridden methods that throw 'not supported', and surprising behavior in polymorphic code.

A replacement driver

If a company says 'any licensed driver can drive this delivery van', a substitute driver must be able to do everything the job requires. A driver who refuses to reverse breaks the promise, even if they are technically a driver.

02

When to use it

  • Designing any inheritance hierarchy or interface implementation.
  • Reviewing subclasses that override behavior.
  • Debugging polymorphic code that behaves differently for one subtype.
03

Where it shows up in interviews

Hierarchy validation

Recognize it when: subclass throws or ignores parent methods.

  • Refactor Bird with Penguin.fly()
  • Refactor Square extends Rectangle
Contract design

Recognize it when: implementations behave inconsistently.

  • Design a Repository interface
  • Design a Cache interface
04

Where it is used in real software

Java Collections UnsupportedOperationException

List.of returns lists that throw on add, a known LSP compromise in the JDK; callers must know which lists are immutable.

Stack extends Vector

Java's Stack inherits Vector's random-access methods, letting callers break stack semantics.

Contract tests

Teams write shared test suites every implementation of an interface must pass.

05

Key terms

Substitutability
A subtype works anywhere the base type does.
Precondition
What must be true before calling; subtypes cannot strengthen it.
Postcondition
What is guaranteed after; subtypes cannot weaken it.
Invariant
Always-true property; subtypes must preserve it.
Contract test
Shared tests run against every implementation.
06

How it works, step by step

  1. 1
    Write down the base contract

    Inputs accepted, outputs promised, errors thrown, invariants.

  2. 2
    Check each override

    Does it accept at least the same inputs and promise at least the same results?

  3. 3
    Look for red flags

    Throwing not-supported, empty overrides, instanceof checks by callers.

  4. 4
    Redesign the hierarchy

    Split interfaces or use composition.

  5. 5
    Add contract tests

    Run the same tests against all implementations.

07

Square extends Rectangle

Code: r.setWidth(5); r.setHeight(4); expect area 20

Step 1 / 2
ObjectAfter setWidth(5)After setHeight(4)area()Contract holds?
Rectangle5 x ?5 x 420Yes
Square5 x 54 x 416No: width changed unexpectedly

NOWObject: Rectangle | After setWidth(5): 5 x ? | After setHeight(4): 5 x 4 | area(): 20 | Contract holds?: Yes

A square is a rectangle in geometry, but not a substitutable mutable Rectangle object. Fix: separate immutable Shape types with area(), no shared setters.

08

Implementation

// Violation: Penguin cannot honor Bird.fly()abstract class Bird { abstract void fly(); }class Penguin extends Bird {    void fly() { throw new UnsupportedOperationException(); } // breaks callers} // Fix: model capabilities separatelyinterface Bird2 { String name(); }interface Flyer { void fly(); } record Sparrow(String name) implements Bird2, Flyer {    public void fly() { System.out.println(name + " flies"); }}record Penguin2(String name) implements Bird2 {} void migrate(List<Flyer> flyers) { flyers.forEach(Flyer::fly); } // only accepts things that can fly
09

Complexity and performance

Contract tests1 suite x n implementations

Cheap insurance.

Violation costBugs in every polymorphic caller

Hard to trace.

10

Trade-offs

Modeling reality vs behavior

Real-world is-a relationships do not always mean behavioral substitutability; model behavior.

Pragmatic exceptions

Some libraries accept small LSP compromises (immutable lists) for simplicity; document them clearly.

11

Variants and related techniques

Design by contract

Explicit preconditions, postconditions, and invariants (Eiffel, assertions).

Variance rules

Parameter types can be contravariant and return types covariant in subtypes.

12

Common mistakes

  • Overrides that throw 'not supported'.

    Fix: Split the interface so the subtype does not claim the capability.

  • Subtypes adding stricter validation.

    Fix: Callers of the base type will be surprised; keep preconditions equal or weaker.

13

Interview questions

Why is Square extends Rectangle an LSP violation?

Rectangle's contract lets width and height change independently. Square must keep them equal, so code relying on Rectangle's behavior gets wrong results when given a Square.

How do you detect LSP violations?

Look for overrides that throw or do nothing, callers that check concrete types, and implementations that fail shared contract tests.

14

Practice problems

ProblemDifficultyWhat it trains
Fix the Bird/Penguin hierarchyEasyCapability interfaces.
Write contract tests for a Repository interfaceMediumBehavioral contracts.