What Is the Liskov Substitution Principle (LSP)?

Ready to start learning? Individual Plans →Team Plans →

Code can compile cleanly and still fail in production the moment a subclass is used through its base type. That is the problem the barbara liskov substitution principle definition source is meant to solve: if a subtype cannot safely replace its parent, the design is already broken.

Quick Answer

The Liskov Substitution Principle (LSP) says any subclass should be usable anywhere its superclass is expected without breaking client code. In practice, that means the subtype must honor the parent type’s behavior, inputs, outputs, and side effects. If callers need special-case logic, the inheritance design is violating LSP.

Quick Procedure

  1. Inspect the base type’s contract and list what callers expect.
  2. Check each subclass for narrower inputs, changed outputs, or extra side effects.
  3. Search client code for type checks, switches, and subclass-specific branching.
  4. Run the same behavior tests against every subtype.
  5. Refactor broken inheritance into composition or smaller interfaces.
  6. Document the contract clearly so future subclasses do not drift.
TopicLiskov Substitution Principle (LSP)
Core IdeaA subtype must be replaceable for its base type without surprising client code
First Formalized ByBarbara Liskov in 1987 as a subtyping principle
Common Failure SignsType checks, unsupported operation exceptions, narrower inputs, and behavior changes
Best FixRedesign the abstraction, split the contract, or use composition
Why It MattersSafer refactoring, fewer bugs, and cleaner object-oriented systems

Understanding the Liskov Substitution Principle

The Liskov Substitution Principle is the rule that a subclass must be usable anywhere the superclass is expected without forcing the caller to change its behavior. That sounds abstract until you see a base type that promises one thing and a subtype that quietly does another.

Barbara Liskov introduced the idea in 1987, and the formal intent is still the same: subtypes must preserve the properties that clients depend on. In plain English, a parent type is a promise, not just a code-sharing mechanism. If a method says it accepts a value, returns a result, or guarantees a state change, the subtype should respect that contract.

This is why LSP is about behavior, not just syntax. A subclass can compile, inherit methods, and pass type checks while still breaking client code because its meaning is different. The Microsoft Learn documentation on object-oriented design repeatedly emphasizes that interfaces and base classes should describe predictable behavior, not just shared method names.

“A subtype must be substitutable for its base type, not merely related to it.”

LSP sits inside the SOLID principles as the part that keeps inheritance honest. If the base type is too broad, or if a subclass needs special treatment, the design is telling you the abstraction is wrong. That is why developers often search for liskov substitution principle authoritative source material when a hierarchy starts producing awkward conditionals and brittle tests.

What substitutability really means

Substitutability means client code should not need to ask, “Which subclass is this?” If it does, the subclass probably does not behave like the parent in a reliable way. Good inheritance removes special cases from the caller instead of pushing them into every use site.

Think of a payment processor, a file store, or a shape class. If the caller can use the base type with no branching, no defensive checks, and no workaround logic, the abstraction is doing its job. If not, the hierarchy is likely teaching callers to fear polymorphism.

Note

LSP is not a style preference. It is a design constraint that protects client code from hidden behavior changes.

Why LSP Matters in Real Code

LSP violations create bugs that are hard to spot in code review because the problem is often invisible at the type level. A class can extend another class, override methods, and still break the assumptions that other code relies on. That is how you get “works in tests” behavior that falls apart in the real application flow.

One common sign is brittle branching around type checks. When client code starts using instanceof, switch statements, or downcasts, it is usually compensating for an abstraction that failed. That is extra complexity added just to survive a subtype that does not behave like the parent.

This matters across Java, C++, C#, and any language with inheritance or interface-based polymorphism. It also matters in API design, where one bad subtype can force every caller to add guard clauses. In practice, LSP protects maintainability because a stable contract makes refactoring less risky and testing less repetitive.

The business impact is real. The ISO/IEC 27001 framework stresses consistent control behavior, and software design has the same need for consistency. When object contracts are unreliable, teams spend more time on defensive coding, bug triage, and patching edge cases than on adding value.

  • Fewer hidden defects because callers can trust the type contract.
  • Less special-case code because the caller does not need subtype logic.
  • Safer refactoring because changing internals does not change meaning.
  • Cleaner team boundaries because APIs are easier to use correctly.

For a wider industry lens, the U.S. Bureau of Labor Statistics continues to project strong demand across software and IT roles, which means maintainable code is not optional. Poor inheritance design becomes expensive fast when more people depend on it.

What Does a Behavioral Contract Mean?

A behavioral contract is the set of promises a type makes to its callers: what inputs are valid, what outputs mean, what side effects happen, and what state stays stable. LSP is really about preserving that contract across the inheritance chain. If a subtype changes the rules, it may still compile, but it is no longer truly substitutable.

There are three parts to watch closely. Preconditions are what callers must satisfy before they invoke a method. Postconditions are what the method guarantees after it runs. Invariants are the rules that must remain true throughout the object’s life, such as “balance never goes below zero” or “a read-only operation does not change state.”

A subtype can usually weaken a precondition, strengthen a postcondition, or preserve an invariant. It should not do the opposite. For example, if a base class accepts any positive quantity, a subclass that rejects half of those values is narrowing the contract and forcing the caller to understand the subtype. That is exactly the kind of hidden change that breaks explain liskov substitution principle examples in real codebases.

  1. Inputs: The subtype should accept at least what the parent accepts.
  2. Outputs: The subtype should return results that still satisfy the parent’s promise.
  3. State: The subtype should not break object invariants or surprise callers with extra mutation.
  4. Errors: The subtype should not introduce new failure modes for valid parent use cases.

Side effects matter too. A method that suddenly logs differently, mutates shared state, requires extra setup, or throws new exceptions can be enough to break substitutability. Contract mismatch is not just about return values; it is about the full behavior clients depend on.

Common Signs of an LSP Violation

An LSP violation often shows up as code smell long before it becomes a production incident. The most obvious clue is client code full of special-case logic. If callers must check which subclass they received, the hierarchy is making the caller do the work that the type system was supposed to handle.

Another warning sign is a subclass that throws UnsupportedOperationException or silently no-ops on a method the base type promised. That means the parent contract was broader than the subtype could honestly support. The caller thinks it is using a valid object, but the object is refusing to behave.

Look for narrower input rules, changed meaning, or altered performance characteristics. A parent type that accepts a range of values should not have a subtype that rejects part of that range unless the abstraction was split correctly. A subtype that is “functionally correct” but much slower may also break callers if timing is part of the expected behavior.

  • Type checks in client code: instanceof, pattern matching, or class-name switches.
  • Subclass exceptions: Methods that exist in the base type but are disabled in children.
  • Reduced capability: A subtype that accepts fewer valid inputs than the parent.
  • Meaning changes: The same method name now produces different semantics.

When you explain lsp to a team, this is the practical version: if a subclass forces callers to remember its quirks, the abstraction failed. Tests can still pass if they only cover the happy path. LSP problems usually appear when real data, real edge cases, or real integration code enters the picture.

Classic and Practical Examples of LSP Failures

The classic rectangle and square example is still useful because it exposes a common mistake: inheritance based on taxonomy instead of behavior. A square is a rectangle in geometry, but that does not automatically make it a safe subtype in code if the rectangle contract allows independent width and height changes. If setting width also changes height, client code that expects rectangle behavior breaks.

The same problem appears in a Dog and Animal hierarchy when the base class promises actions that do not apply cleanly to every subtype. A flying animal, for example, may not be a valid replacement for a bird if callers expect actual flight behavior and not a placeholder method. The key question is not “is it related?” but “does it preserve the promise?”

Another common failure is a moveable object that exposes move(), setPosition(), or translate(), but a subclass cannot support one of those operations. If the subtype disables the method or changes its meaning, the caller has to compensate. That is not polymorphism; it is a trap.

Here is the practical lesson: inheritance should model behavior, not just labels. If you are using interfaces or abstract classes to capture a role, every implementation must honor the same expectations. Otherwise, the abstraction becomes a source of false confidence.

Example: a read-only subtype that still mutates state

A read-only object should not change observable state when a caller asks for data. If a subclass caches results by mutating shared fields, logs in a way that affects timing, or increments counters that matter to the caller, it may violate the contract. Even small changes can break audit logic, test repeatability, or concurrency assumptions.

This is why developers often ask for liskov substitution principle definition reputable source material when they are cleaning up a design review. The definition is simple, but the implementation details are where teams get burned.

How to Detect LSP Problems in Your Codebase

Detecting LSP problems starts by reading the base type as a contract document, not just a code container. Ask what the type promises, what callers assume, and which behaviors must stay consistent across all implementations. If the base type is vague, the hierarchy is already too broad.

Next, inspect client code for defensive branching. A healthy design rarely needs type checks, subtype-specific exception handling, or duplicated logic that exists only because some subclasses are unreliable. When callers are forced to work around the type system, the abstraction is leaking.

Testing is one of the fastest ways to expose the problem. Run the same behavior-focused tests against every implementation of a base class or interface. If one subclass fails valid scenarios that the parent type suggests are acceptable, you have found a contract mismatch.

  1. Review the contract: Write down inputs, outputs, side effects, and invariants for the base type.
  2. Scan client code: Search for instanceof, casts, and special branches.
  3. Run shared tests: Apply the same test suite to each subtype or implementation.
  4. Check overrides: Identify methods overridden only to reject input or disable behavior.
  5. Interview the call sites: Ask what assumptions real callers make about the object.

A good code review question is simple: “Can this subclass safely replace the base class everywhere it is used?” If the answer is “only sometimes,” the design is not substitutable enough. That is the moment to redesign, not to add another workaround.

Pro Tip

Search for type checks first. If code keeps asking what subclass it has, your inheritance model is probably too fragile.

How Do You Fix LSP Violations?

The best fix for an LSP violation is usually not “override harder.” It is to redesign the abstraction so the contract matches reality. If a subtype cannot honor the parent promise, the hierarchy needs to change.

The most common fix is to replace inheritance with composition. Use composition when the relationship is “has-a” rather than a true behavioral “is-a.” For example, a class can use a pricing strategy, storage engine, or movement behavior object without pretending that every implementation fits into one giant base class.

Another effective fix is to split a bloated base class into smaller abstractions. If one parent type promises too many operations, some subclasses will always be forced to fake or disable part of the contract. Smaller interfaces reduce that pressure and make substitutability much easier to preserve.

  1. Refactor the hierarchy: Remove subtype relationships that are only conceptual, not behavioral.
  2. Split the contract: Separate optional capabilities into focused interfaces.
  3. Use composition: Delegate behavior instead of forcing inheritance.
  4. Clarify the API: Make invalid states explicit with validation or separate types.
  5. Retest clients: Verify that call sites no longer need subtype-specific handling.

This is also where refactoring should be deliberate, not cosmetic. You are not just moving methods around. You are reshaping the contract so clients can trust the abstraction again.

How to Choose an LSP-Friendly Design

Choosing an LSP-friendly design means starting with behavior the client needs instead of starting with a parent class and hoping subclasses will fit. That is the difference between a solid abstraction and a fragile inheritance tree. The design should make the right thing easy and the wrong thing impossible or explicit.

Keep base types narrow. The more promises a type makes, the harder it is for every subtype to honor all of them consistently. A small interface with one clear responsibility is much easier to substitute than a “god object” with dozens of methods that only some children can support.

Design for polymorphism intentionally. The goal is not just code reuse. The goal is interchangeable behavior. If two classes share code but not behavior, inheritance may be the wrong tool.

Broad base typeOften creates forced overrides, disabled methods, and special-case callers
Focused abstractionMakes the contract smaller, clearer, and easier to substitute safely

Document the contract clearly. State what inputs are valid, what failures mean, and whether methods mutate state. This is especially important for teams that rely on interfaces and abstract classes, because those types can look safe while still hiding assumptions that concrete implementations cannot honor.

If you need a current ecosystem example, the Center for Internet Security Benchmarks show how precise standards reduce ambiguity. Software contracts benefit from the same discipline.

LSP in Interfaces and Abstract Classes

LSP applies to interfaces and abstract classes just as much as it applies to concrete inheritance. An interface can still be poorly designed if implementers are forced to fake behavior, throw exceptions, or return meaningless defaults. A type name is not enough; the behavior has to be real.

Abstract classes can be even trickier because they often provide partial implementations that assume too much. If a base class method calls an abstract hook in a specific sequence, every subclass must preserve that sequence or the contract falls apart. Default methods can help with shared behavior, but they can also create a false sense of completeness when not every implementation truly fits.

The main risk is that developers confuse compile-time compatibility with runtime substitutability. Just because a class satisfies the compiler does not mean it satisfies the caller. That is why the Liskov Substitution Principle remains a design concern, not merely a type-checking concern.

  • Interfaces: Good when they describe a real capability every implementation can support.
  • Abstract classes: Useful when shared behavior is valid across all descendants.
  • Default methods: Helpful when they do not hide incompatible assumptions.
  • Red flag: Any implementation that must lie, fake, or disable an inherited promise.

The Java ecosystem, for example, often exposes these design tradeoffs clearly because interfaces and inheritance are both common. If the abstraction is honest, polymorphism stays simple. If not, every implementation becomes a special case.

How Do You Test for Substitutability?

Testing substitutability means verifying the behavior clients rely on, not just individual method outputs. A good test suite for LSP should be reusable across every subtype so that each implementation is judged by the same contract. If one subtype needs a different test suite, the abstraction is probably inconsistent.

Shared test contracts are especially useful for interface implementations and abstract base classes. You can define a suite that checks valid inputs, invalid inputs, state transitions, and edge cases once, then run it against every implementation. This is a practical way to catch contract drift before it spreads through the codebase.

Include tests that matter to callers. If a caller depends on immutability, verify that state does not change. If a caller depends on exceptions for invalid input, verify that the same invalid input behaves consistently across all implementations. If a caller depends on order, timing, or idempotence, those behaviors should be part of the contract test.

  1. Build a shared test suite: Reuse the same behavior checks for every subtype.
  2. Test valid and invalid inputs: Confirm each implementation honors the same boundaries.
  3. Check state transitions: Verify setup, use, and teardown behavior.
  4. Assert observable side effects: Make sure mutation, logging, and exceptions are consistent.
  5. Review failures by design: A failing shared test often reveals the contract, not just the code.

The NIST approach to controlled, repeatable validation is a good mental model here. Behavior should be measurable, repeatable, and consistent. Substitutability is no different.

LSP, SOLID, and the Bigger Design Picture

LSP is the connective tissue that makes the rest of SOLID work cleanly. If subtypes cannot be substituted safely, then the Open/Closed Principle starts to fail because every new subtype forces changes in callers. That is a strong signal that the architecture is coupling code where it should be decoupling it.

Violating LSP usually increases conditionals, duplication, and defensive programming. Those are not just style issues. They are indicators that the design is leaking implementation detail into places that should only depend on the contract. The result is harder maintenance, more fragile tests, and slower feature work.

Respecting LSP also improves readability. A developer should be able to look at a base type, use it confidently, and trust that any implementation behaves correctly. That trust matters because good design is not about cleverness. It is about reducing the number of things a caller has to remember.

  • Open/Closed Principle: Works better when new subtypes do not force caller changes.
  • Single Responsibility Principle: Smaller types are easier to substitute correctly.
  • Interface Segregation Principle: Focused contracts reduce fake implementations.
  • Dependency Inversion: Callers depend on stable abstractions, not fragile details.

For a broader standards reference, the CompTIA workforce research continues to show how much modern IT work depends on reliable systems and maintainable architecture. Design principles like LSP are not academic extras; they are what keep large systems usable.

Key Takeaway

  • LSP means replaceability: A subtype should work anywhere the base type works.
  • Behavior beats inheritance: Shared code does not justify a subtype relationship.
  • Type checks are a warning sign: Callers should not need special-case logic for subclasses.
  • Fix the abstraction, not the caller: If substitution fails, redesign the hierarchy or use composition.
  • Shared tests catch drift early: Behavior-based test suites expose contract mismatches fast.

Conclusion

The Liskov Substitution Principle is simple to state and easy to violate. A subclass must preserve the behavior, expectations, and contract of its superclass, or it is not truly substitutable. Compilation success is not enough.

When you see type checks, unsupported operations, narrowed inputs, or surprise side effects, treat them as design failures, not just implementation bugs. The right fix is usually to split the abstraction, move to composition, or narrow the contract until every subtype can honestly support it.

If you are reviewing a codebase right now, start with one question: can this subclass safely replace the base class everywhere it is used? If the answer is no, the caller should not have to work around it. The design needs to change.

CompTIA®, Microsoft®, Oracle®, and Cisco® are trademarks of their respective owners.

[ FAQ ]

Frequently Asked Questions.

What is the main goal of the Liskov Substitution Principle (LSP)?

The primary goal of the Liskov Substitution Principle is to ensure that objects of a subclass can replace objects of a superclass without altering the correctness of the program. This promotes a more flexible and maintainable object-oriented design.

By adhering to LSP, developers can extend classes confidently, knowing that the new subclasses will behave as expected when used through the parent class interface. This reduces the risk of introducing bugs and makes the code more predictable and robust.

How does Liskov’s principle help in designing better class hierarchies?

Implementing Liskov’s principle encourages careful consideration of class relationships and behaviors, leading to cleaner and more logical inheritance structures. It ensures subclasses do not violate the expectations set by their parent classes.

This results in a hierarchy where subclasses can be substituted seamlessly, enabling polymorphism to work effectively. As a consequence, developers can extend systems with new classes without modifying existing code, fostering scalability and flexibility.

What are common violations of the Liskov Substitution Principle?

Violations often occur when subclasses override methods in a way that changes expected behaviors, such as reducing preconditions or increasing postconditions. For example, a subclass that throws exceptions not expected by the superclass or narrows input constraints can break LSP.

Other violations include adding new state or behaviors that aren’t compatible with the superclass’s interface, or failing to honor the contract established by the parent class. These violations can cause runtime errors or unexpected behavior when subclasses are used interchangeably with superclasses.

Can you give an example of a violation of the Liskov Substitution Principle?

Suppose you have a base class called Rectangle with methods to set width and height. A subclass Square overrides these methods to ensure width and height are always equal. This violates LSP because substituting Square for Rectangle changes the expected behavior of setting width and height independently.

In this case, code expecting a Rectangle might break when a Square is used, because the constraints on the subclass differ from the base class’s behavior. This illustrates how improper subclassing can violate LSP and lead to bugs or inconsistent behavior.

What are best practices for ensuring compliance with the Liskov Substitution Principle?

To adhere to LSP, developers should ensure subclasses honor the contracts established by their superclasses, including preconditions, postconditions, and invariants. This often involves designing base classes with clear, minimal, and well-defined interfaces.

Additionally, using techniques such as interface segregation, composition over inheritance, and thorough testing can help identify and prevent violations. Regular code reviews and adherence to design principles like SOLID further promote compliance with LSP, resulting in more reliable and maintainable object-oriented systems.

Related Articles

Ready to start learning? Individual Plans →Team Plans →
Discover More, Learn More
What is Interface Segregation Principle (ISP) Discover how mastering the Interface Segregation Principle can improve your code quality… What Is (ISC)² CCSP (Certified Cloud Security Professional)? Discover how to enhance your cloud security expertise, prevent common failures, and… What Is (ISC)² CSSLP (Certified Secure Software Lifecycle Professional)? Learn about the (ISC)² CSSLP certification to enhance your secure software development… What Is 3D Printing? Learn how 3D printing accelerates prototyping and custom part production by building… What Is (ISC)² HCISPP (HealthCare Information Security and Privacy Practitioner)? Discover how earning the (ISC)² HCISPP certification enhances your healthcare cybersecurity expertise,… What Is 5G? Discover how 5G enhances mobile connectivity by providing faster speeds, lower latency,…
FREE COURSE OFFERS