Skip to main content
Articles/Java/liskov-substitution-principle-solid-demystified
Series: Low-Level Design Interview Prep•Part 3 of 17
Scenario Article

Liskov Substitution Principle Demystified: The Heart of Polymorphism

The Fragile Base Class: A Common Inheritance Nightmare

Abstract Algorithms•Jun 18, 2026
💡 Executive Summary

The Liskov Substitution Principle (LSP) mandates that subclasses must be substitutable for their superclasses without changing the correctness of the program.

🎯 System Scenario & Requirements

System Goal

The Liskov Substitution Principle (LSP) mandates that subclasses must be substitutable for their superclasses without changing the correctness of the program.

Actors & Components

  • Application or API tier
  • Core service under design
  • Operator or SRE observer

Functional Requirements

  • TLDR: The Liskov Substitution Principle (LSP) mandates that subclasses must be substitutable for their superclasses without changing the correctness of the program.
  • It shifts our understanding of inheritance from syntax (IS-A) to behavior (behaves-like).
  • Imagine you are maintaining a payment processing backend for an e-commerce platform.

Constraints & Assumptions

  • Strictly adhering to LSP is not free.
  • It requires careful trade-offs between clean design and system complexity: Class Proliferation vs.

⚡ Execution Stages & Decision Trace

1

Stage 1 — Defining the Contract: Behavioral Subtyping

Action / Decision:

The Liskov Substitution Principle (LSP) was formulated by Barbara Liskov in a 1987 conference keynote and later formalized in a 1994 paper with Jeannette Wing.

Component Behavior:

The formal definition states: If for each object $o 1$ of type $S$ there is an object $o 2$ of type $T$ such that for all programs $P$ defined in terms of $T$, the behavior of $P$ is unchanged when $o 1$ is substituted…

Observable Output: In simpler terms, a subclass should extend the parent's behavior, not change or restrict it.
Read Full Stage in Deep Dive ↓
📖

Deep Dive

Detailed Architectural Analysis

Scenario context

The Liskov Substitution Principle (LSP) mandates that subclasses must be substitutable for their superclasses without changing the correctness of the program. It shifts our understanding of inheritance from syntax (IS-A) to behavior (behaves-like).


Imagine you are maintaining a payment processing backend for an e-commerce platform. The system defines a base payment processor class to handle transaction process…

This walkthrough focuses on The Fragile Base Class: A Common Inheritance Nightmare.

Before making a design choice, clarify:

  • Imagine you are maintaining a payment processing backend for an e-commerce platform.
  • The system defines a base payment processor class to handle transaction processing, refunds, and fraud checks.
  • For months, everything runs smoothly.

Concepts in play: jvm-internals-and-garbage-collection, design-patterns-gof, design-principles-solid, database-indexing

Stage 1 — Defining the Contract: Behavioral Subtyping

  • The Liskov Substitution Principle (LSP) was formulated by Barbara Liskov in a 1987 conference keynote and later formalized in a 1994 paper with Jeannette Wing.
  • The formal definition states: If for each object $o 1$ of type $S$ there is an object $o 2$ of type $T$ such that for all programs $P$ defined in terms of $T$, the behavior of $P$ is unchanged when $o 1$ is substituted…
  • In simpler terms, a subclass should extend the parent's behavior, not change or restrict it.
  • If a client class expects an object of type Parent , it should be able to receive an object of type Child and continue operating correctly without needing to inspect the concrete type using instanceof or catching unexpe…

Observable outcome: If a client class expects an object of type Parent , it should be able to receive an object of type Child and continue operating correctly without needing to inspect the concrete type using instanceof or catching unexpe…

Stage 2 — Preconditions, Postconditions, and Invariants

  • To execute LSP successfully, you must master the rules governing preconditions, postconditions, and invariants.
  • Let's look at a concrete toy dataset representing validation constraints to see how modifying these constraints breaks or preserves the contract.
  • 📊 Toy Dataset: Validation Constraint Matrix Consider a base processor that validates customer age inputs.
  • The base class accepts ages between 18 and 100.

Observable outcome: The base class accepts ages between 18 and 100.

Stage 3 — Deep Dive: Covariance, Contravariance, and the History Constraint

  • To fully grasp the mechanics of substitution, we must analyze the type-system level behaviors that occur during dynamic dispatch and inheritance.
  • The Internals of Type Substitution LSP relies on covariant and contravariant types.
  • Covariance allows a method to return a more specific (narrower) type than defined in the superclass.
  • Contravariance allows a method to accept a more general (broader) argument type than defined in the superclass.

Observable outcome: Contravariance allows a method to accept a more general (broader) argument type than defined in the superclass.

Stage 4 — Visualizing the Substitutability Flow

  • Understanding how contracts map between the client, base type, and subclasses is easiest when viewed as a flow.
  • 📊 Client-Subtype Compatibility Workflow This diagram maps how a client system's input is handled by two different subclass implementations.
  • Subclass A weakens the input preconditions (accepts ages 0-120) and safely processes the call.
  • Subclass B strengthens the preconditions (only accepts ages 21-80), resulting in a runtime exception when passed an age of 19, which violates the parent interface contract and crashes the client system.
flowchart TD
    Client[Client System] -->|Invokes method| Base[Base Type Interface]
    Base -->|Preconditions: Age 18-100| Sub1[Subclass A: Age 0-120]
    Base -->|Preconditions: Age 18-100| Sub2[Subclass B: Age 21-80]
    Sub1 -->|Returns ValidationResult| Client
    Sub2 -->|Throws IllegalArgumentException for Age 19| Client
    style Sub1 fill:#d4edda,stroke:#28a745
    style Sub2 fill:#f8d7da,stroke:#dc3545

Observable outcome: Subclass B strengthens the preconditions (only accepts ages 21-80), resulting in a runtime exception when passed an age of 19, which violates the parent interface contract and crashes the client system.

Stage 5 — Real-World Systems: How Frameworks Enforce LSP

  • In enterprise frameworks like Spring and Hibernate, LSP is a foundational assumption.
  • For example, Spring's Dependency Injection container resolves beans by their interface type.
  • If a developer registers a custom implementation of a MailSender interface that throws an exception during startup or fails to resolve template properties, the application context fails to load.
  • Similarly, in Hibernate, lazy-loading proxies subclass your entity classes at runtime to intercept getter calls.

Observable outcome: Similarly, in Hibernate, lazy-loading proxies subclass your entity classes at runtime to intercept getter calls.

Stage 6 — Refactoring an LSP Violation: Java Code Contrast

  • Let us contrast a database repository implementation that violates LSP with its refactored, safe version.
  • The Violation: Throwing Exceptions in Overrides In this database processing example, we have a base CrudRepository class that supports reads, writes, and deletes.
  • We extend it to create a ReadOnlyRepository .
  • Since write and delete actions are not allowed, we throw exceptions.
// VIOLATION: Subclass throws UnsupportedOperationException, breaking parent contract
public class CrudRepository {
    public String findById(String id) {
        return "EntityData";
    }

    public void save(String data) {
        System.out.println("Data saved to database");
    }

    public void delete(String id) {
        System.out.println("Data deleted from database");
    }
}

// Subclass that violates the base CrudRepository contract
public class ReadOnlyRepository extends CrudRepository {
    @Override
    public void save(String data) {
        // Violates LSP: Client expects writes to succeed
        throw new UnsupportedOperationException("Write operations are disabled on read-only databases");
    }

    @Override
    public void delete(String id) {
        // Violates LSP: Client expects deletes to succeed
        throw new UnsupportedOperationException("Delete operations are disabled on read-only databases");
    }
}

// Client application class executing data cleanups
public class DataCleanupService {
    public void purgeRecord(CrudRepository repo, String id) {
        // Safe compiler call, but crashes at runtime if passed ReadOnlyRepository!
        repo.delete(id);
    }
}
// RESOLUTION: Segregate interfaces to ensure safe substitution
public interface ReadRepository {
    String findById(String id);
}

public interface WriteRepository {
    void save(String data);
    void delete(String id);
}

// Implements read capabilities safely
public class DatabaseReader implements ReadRepository {
    @Override
    public String findById(String id) {
        return "EntityData from Reader";
    }
}

// Implements read/write capabilities safely
public class DatabaseWriter implements ReadRepository, WriteRepository {
    @Override
    public String findById(String id) {
        return "EntityData from Writer";
    }

    @Override
    public void save(String data) {
        System.out.println("Data written safely");
    }

    @Override
    public void delete(String id) {
        System.out.println("Data deleted safely");
    }
}

// Client service accepts exactly what it requires
public class SafeDataCleanupService {
    private final WriteRepository writeRepo;

    public SafeDataCleanupService(WriteRepository writeRepo) {
        this.writeRepo = writeRepo;
    }

    public void purgeRecord(String id) {
        // Guaranteed to succeed without runtime contract failures
        writeRepo.delete(id);
    }
}

Observable outcome: Since write and delete actions are not allowed, we throw exceptions.

End-to-end outcome

After working through the stages, you should be able to explain:

  • When writing polymorphically dispatched code, follow these best practices to maintain substitutability: 1.
  • Avoid Exceptions in Overrides: If you find yourself writing throw new UnsupportedOperationException() in a subclass, refactor your interface hierarchy.
  • Behavior Over Syntax: Inheritance is a behavioral contract ("behaves-like"), not just a compiler mapping.
  • Contract Rules: Subclasses can weaken preconditions or strengthen postconditions, but never the reverse.

Alternatives considered

API Decision Guide: Inheritance vs. Composition

When designing APIs, use this decision guide to choose between extending a class or composing it.

Failure and degraded modes

The Cost of Clean Inheritance: Design Trade-offs

  • Detection: Strictly adhering to LSP is not free.
  • User or operator impact: Strictly adhering to LSP is not free.

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit