LSP: the essence of Barbara Liskov’s substitution principle in development

Author: IT Sectr Published: 2026-05-12 Reading time: 9 min

LSP (Liskov Substitution Principle) — the third principle of SOLID, which defines the conditions for correct inheritance in object-oriented programming. The principle was formulated by Barbara Liskov in 1987 and formalized as: if S is a subtype of T, then objects of type T can be replaced by objects of type S without changing the properties of the program. As noted in Robert Martin’s book Clean Architecture (2017), the substitution principle requires that a subclass not weaken the contract of the base class.

Key Takeaways

  • LSP — the Liskov Substitution Principle, the third SOLID principle about correct inheritance
  • Subclass must preserve the base class contract — preconditions and postconditions
  • LSP violation manifests in the «square and rectangle problem» and thrown exceptions
  • Composition is often preferable to inheritance for LSP compliance
  • Design by Contract is a formal way to verify LSP

What is LSP (Liskov Substitution Principle)?

LSP (Liskov Substitution Principle) — the substitution principle formulated by Barbara Liskov at the OOPSLA conference in 1987. Formal definition: let q(x) be a provable property of objects x of type T. Then q(y) must be provable for objects y of type S, where S is a subtype of T. Simply put: objects of a subclass must behave so that code working with the base class continues to work correctly with the subclass as well.

In practice, LSP means that a subclass must not violate the contract of the base class. The contract includes preconditions (what is required to call a method), postconditions (what is guaranteed after the call), and invariants (conditions that persist throughout the object’s lifetime). A subclass may strengthen preconditions or weaken postconditions — this is precisely an LSP violation.

A classic example of LSP violation is a square inheriting from a rectangle. The setWidth method of a rectangle sets the width, while for a square it sets both width and height. A client expecting rectangle behavior (changing one side does not affect the other) gets an unexpected result. A square is not a valid subtype of a rectangle.

Formal conditions of LSP

LSP establishes three conditions for correct inheritance: subclass preconditions cannot be stronger than base class preconditions (the subclass does not require more), subclass postconditions cannot be weaker than base class postconditions (the subclass guarantees no less), and base class invariants must be preserved in the subclass. These conditions are known as the Design by Contract rule according to Bertrand Meyer.

If at least one condition is violated, code using polymorphism may fail. The compiler does not check semantic contracts, only syntactic ones. Therefore, LSP is a matter of architectural discipline, not static typing.

How the Liskov Substitution Principle works

The LSP mechanism is based on behavioral compatibility of types. If class S inherits from class T, client code should be able to use S wherever T is expected without changing its behavior. This includes not only method signatures but also their semantics.

LSP does not forbid a subclass from adding new behavior. It is forbidden to violate the expectations of code written for the base class. If the base class guarantees that the save method does not throw exceptions, the subclass must not throw them. If the base class returns a non-negative value, the subclass must not return a negative one.

In real projects, LSP is most often violated when adding conditional logic in subclass methods: «if condition — throw an exception», «if condition — return null». Each such «surprise» undermines polymorphism and forces client code to check the object type before calling — which contradicts the very idea of object-oriented design.

In mobile projects, a typical LSP violation occurs when creating base ViewModels. If BaseViewModel guarantees that the onCleared method releases all resources, and a subclass overrides this method as empty — any code relying on resource cleanup through a polymorphic onCleared call will work incorrectly. LSP requires that the subclass either calls super.onCleared() or performs the same work itself. Composition via LifecycleObserver is an alternative that eliminates LSP violation in lifecycle management.

Signs of LSP violation in code

Key indicators of LSP violation include: checking the object type via instanceof or is before calling a method, empty method implementations (stubs), throwing NotImplementedError or UnsupportedOperationException, returning null instead of a value. Each of these patterns signals that the subclass is not a valid subtype.

Another common sign is inheritance aimed at code reuse rather than modeling an «is-a» relationship. Class Bird has a fly() method. Class Penguin inherits from Bird and overrides fly() as empty or throwing an exception. This is an LSP violation: a penguin is not a valid subtype of bird.

In mobile development, LSP is violated when creating base ViewHolder, Fragment, or ViewController classes with stub methods. If a subclass does not use half of the base class methods — inheritance was chosen incorrectly. Composition or interface segregation solves the problem more correctly.

LSP test

A simple test for checking LSP: write a unit test for the base class that verifies its contract (return values, exceptions, side effects). Run this test for every subclass. If the test fails — LSP is violated. This approach is called «testing through the base class contract».

In Android projects, such a test is useful for ViewModel and Repository. If BaseViewModel guarantees a Loading state before an error, and a subclass throws an error without Loading — the test will catch the LSP violation at the CI stage.

LSP examples in mobile development

Let’s look at an Android example with ClickListener handling. An LSP violation occurs when the base implementation guarantees something and the subclass violates it.

kotlin
// Base class with guarantee: onClick will be called
open class BaseClickListener {
    open fun onClick(view: View) {
        // basic handling
    }
}

// LSP violation: subclass adds a condition throwing an exception
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Correct solution: contract not violated
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

An iOS example with the DataSource protocol demonstrates LSP violation by returning nil instead of data:

swift
// Protocol with contract: returns data or error
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// LSP violation: returns nil without error
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // empty array instead of error
    }
}

// Correct LSP compliance
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

A practical rule: if a subclass cannot fulfill the base class contract, it should not be a subclass. An alternative is to extract an interface with a minimal contract and implement it in each type in its own way.

LSP and inheritance: when to choose composition

Composition is preferable to inheritance in situations where the «is-a» relationship is ambiguous or conditional. A classic example: is a Manager an Employee? Yes. But is a Square a valid Rectangle? LSP says «no». If you doubt the correctness of inheritance — choose composition.

In mobile development, composition is often used through dependency injection: instead of inheriting behavior from a base class, a class receives it through a constructor. A ViewModel does not inherit from Repository but accepts it as a dependency. This eliminates LSP violation by definition — no inheritance, no contract violation.

Signs that inheritance should be replaced with composition: the subclass does not use some base class methods, the subclass overrides methods with empty stubs, client code checks the object type via instanceof. In these cases, inheritance was chosen incorrectly and LSP is violated.

Solution through interfaces

Interfaces solve the LSP problem without inheritance: each type implements exactly the methods it needs. Instead of a common base class Bird with a fly() method (where Penguin cannot fly) — a Flyable interface that only flying birds implement. Penguin implements Bird without a fly() method — LSP is not violated.

In Android architecture, this approach is applied through segregated UseCase interfaces: instead of one large UseCase with getAll, getById, save, delete methods — separate GetItemsUseCase and SaveItemUseCase interfaces. A client depends only on the needed interface, and any class implementing that interface is correct from the LSP perspective.

Frequently Asked Questions

How is LSP different from plain inheritance?

Inheritance is a language mechanism; LSP is a rule for correct use of that mechanism. Inheritance guarantees signature compatibility (syntax); LSP requires behavioral compatibility (semantics). Inheritance without LSP gives polymorphism that breaks at runtime.

Does null in a subclass always violate LSP?

If the base class guarantees a non-null return — yes. If the contract allows null (optional value) — no. LSP does not forbid null; it forbids weakening the contract. Study the base class documentation and check whether the subclass contract is compatible.

How does LSP apply to protocols in Swift?

LSP applies to protocols the same way as to classes. A protocol implementation must comply with the semantic contract: if a protocol defines a method as non-throwing, the implementation must not throw errors. Swift does not check this at the compiler level — the responsibility lies with the developer.

Can LSP be violated when using sealed classes?

Sealed classes in Kotlin are a special case because the hierarchy is closed and known to the compiler. LSP applies to sealed classes to a lesser extent because all subtypes are explicitly enumerated in the when expression. A sealed subclass error will be local rather than a hidden polymorphic error.

How to test LSP compliance in a project?

Write a parameterized test for the base class that runs for all its subclasses. The test verifies key behavioral contracts: return values, exceptions, states. If the test fails on one of the subclasses — LSP is violated. In CI, such a test prevents regression of polymorphic code.

Summary

  • LSP (Liskov Substitution Principle) — the substitution principle, third in SOLID, about semantic compatibility of inheritance
  • Subclass must preserve the base class contract: preconditions, postconditions, and invariants
  • instanceof checks and empty method overrides are the main signs of LSP violation
  • Composition and interfaces solve the LSP problem where inheritance is incorrect
  • The square and rectangle problem is a classic example of subtype incompatibility
  • A contract test for the base class, run for all subclasses, detects LSP violation at CI
  • Sealed classes in Kotlin reduce LSP risks through a closed hierarchy known to the compiler

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also