Liskov Substitution Principle Explained for Developers





    This site is protected by reCAPTCHA and the Google
    Privacy Policyand Terms of Service apply.

    Message Sent!

    Thank you for reaching out. We’ll get back to you shortly.

    Not all inheritance is safe. A subclass may satisfy a type relationship while still violating the expectations set by its parent. When this happens, polymorphism stops being reliable. The Liskov Substitution Principle (LSP) defines the condition under which inheritance remains correct: a subclass must be fully substitutable for its base class without changing program behavior. If substitution breaks correctness, the abstraction itself is flawed.

    In this part of our SOLID Principles series, we focus on behavioral correctness, an often overlooked aspect of object-oriented design, and a critical requirement for making Open–Closed Principle-based extensions truly safe and reliable.

    LSP builds directly on these ideas. While OCP encourages extension through abstraction, LSP ensures that these extensions behave correctly when substituted in place of their parent types. Without LSP, even well-designed OCP-based systems can fail in subtle and hard-to-detect ways.

    What Is the Liskov Substitution Principle?

    The Liskov Substitution Principle (LSP) states that “Objects of a Superclass should be replaceable with objects of their subclass without affecting the correctness of the program”. In simpler terms, if a class B is a subclass of class A, then wherever an object of A is used, an object of B should be usable without changing the expected behavior of the system.

    LSP is not just about inheritance; it is about behavioral consistency. A subclass should honor the contract defined by its parent class, including:

    • Expected behavior
    • Valid inputs
    • Output guarantees
    • Side effects

    If replacing a parent object with a child object causes unexpected results, then the design violates the Liskov Substitution Principle. Let’s understand this with an example. 

    Imagine a system that works with a general concept called Bird.

    • A Bird is expected to be able to fly
    • Many birds, such as Sparrows, can fly
    • However, some birds, like Penguins, cannot fly

    If Penguin is implemented as a subclass of Bird, and the system assumes that all birds can fly, substituting a Penguin where a Bird is expected will cause incorrect behavior.

    Why Does This Violate LSP?

    • The base class (Bird) defines an expectation: flying
    • The subclass (Penguin) cannot fulfill that expectation
    • Replacing a Bird with a Penguin breaks the system’s assumptions

    This means that even though Penguin is a bird in the real world, modeling it this way in software violates LSP.

    The Liskov Substitution Principle reminds us that inheritance should represent true behavioral compatibility, not just conceptual similarity. A subclass should extend a base class without weakening its guarantees or changing its expected behavior.

    Application of the Liskov Substitution Principle (LSP)

    Applying the Liskov Substitution Principle means designing inheritance hierarchies where subclasses can safely replace their parent classes without breaking system behavior. In practice, this requires careful attention to what a base class promises and ensuring that every subclass honors that promise.

    LSP is commonly applied when:

    • Using inheritance and polymorphism
    • Designing frameworks, libraries, or extensible systems
    • Replacing conditional logic with object-oriented abstractions

    Let’s understand this with a simple and practical example.

    LSP Violation (Incorrect Design)

    Consider a system that works with a base class Bird.

    class Bird {
        void fly() {
            System.out.println("Bird is flying");
        }
    }
    
    class Sparrow extends Bird {
        // Sparrow can fly
    }
    
    class Penguin extends Bird {
        void fly() {
            throw new UnsupportedOperationException("Penguins cannot fly");
        }
    }

    Now, imagine client code:

    void makeBirdFly(Bird bird) {
        bird.fly();
    }

    What goes wrong?

    • makeBirdFly() expects any Bird to fly
    • Passing a Penguin breaks the program
    • Penguin cannot safely substitute Bird

    This design violates LSP.

    Applying LSP (Correct Design)

    To apply LSP properly, we need to separate behaviors and design abstractions that represent true guarantees.

    Step 1: Define a more accurate base abstraction

    abstract class Bird {
        void eat() {
            System.out.println("Bird is eating");
        }
    }

    Step 2: Introduce behavior-specific abstraction

    interface Flyable {
        void fly();
    }

    Step 3: Implement correct behavior in subclasses

    class Sparrow extends Bird implements Flyable {
        public void fly() {
            System.out.println("Sparrow is flying");
        }
    }
    
    class Penguin extends Bird {
        // Penguin does not fly
    }

    Step 4: Client code uses the correct abstraction

    void makeBirdFly(Flyable bird) {
        bird.fly();
    }

    Why This Applies to LSP Correctly

    • Flyable guarantees the ability to fly
    • Any class implementing Flyable can be safely substituted
    • No unexpected runtime failures
    • Behavior matches expectations

    This design ensures that substitution does not break correctness, which is the core idea behind the Liskov Substitution Principle.

    When NOT to Apply LSP?

    Inheritance is a powerful feature in object-oriented programming, but it is also one of the most commonly misused. The Liskov Substitution Principle helps us recognize situations where inheritance should be avoided, even if a relationship appears valid at first glance.

    1. When the subclass cannot fully honor the parent’s behavior

    If a subclass cannot support all the behaviors expected from its parent class, inheritance is the wrong choice. A subclass that weakens or removes functionality breaks the expectations defined by the base class. For instance, a class that overrides a method to throw an exception or return unexpected values violates LSP and should not inherit from the parent.

    2. When the relationship is “has-a” instead of “is-a”

    Inheritance should represent a true “is-a” relationship, not just code reuse. If the relationship is better described as “has-a”, composition should be preferred. For example, a Car has an Engine, but a Car is not an Engine. Using inheritance here would be incorrect.

    3. When behavior varies significantly between subclasses

    If subclasses require drastically different behavior, forcing them under the same parent class often leads to conditional logic or overridden methods that do nothing. This is a strong signal that inheritance is being misapplied.

    In such cases, interfaces or composition provide better flexibility and align more closely with LSP.

    4. When changes in the base class can break subclasses

    Inheritance creates tight coupling between parent and child classes. If modifying the base class frequently causes failures in subclasses, it indicates that the inheritance hierarchy is fragile and violates LSP. Stable abstractions are essential for safe substitution.

    Conclusion: Designing Inheritance That Truly Works

    The Liskov Substitution Principle reminds us that inheritance is not just about sharing code; it is about preserving behavior. A well-designed subclass should be able to stand in for its parent class without breaking assumptions, changing expected outcomes, or introducing unexpected failures. These ideas reinforce an important lesson: correctness and reliability matter more than conceptual hierarchy.

    By applying LSP, developers can design systems that are easier to reason about, safer to extend, and more resilient to change. As systems grow, classes often depend on interfaces that contain more methods than they actually need. This leads to unnecessary coupling and forces implementations to support behavior they don’t use. The Interface Segregation Principle (ISP) addresses this issue by promoting smaller, more focused interfaces.

    In the next part of our SOLID Principles blog series, we’ll explore how ISP helps reduce dependency complexity, improve flexibility, and work hand-in-hand with LSP to create clean, maintainable designs.





      This site is protected by reCAPTCHA and the Google
      Privacy Policyand Terms of Service apply.

      Message Sent!

      Thank you for reaching out. We’ll get back to you shortly.

      More Blogs