Latest Post: 15 Examples of Technical Debt in UI/UX Design (and How to Avoid It)

The L in SOLID: Liskov Substitution Principle,

Creating maintainable and scalable code with the Single Responsibility Principle

7 min read
The letters SOLID written in big letters and then a very short description of each principle: Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, Dependency Inversion Principle

Welcome to the third part of the SOLID principles series! If you missed the previous parts, you can check them out here:

In this article, we will explore the Liskov Substitution Principle, the L in SOLID. Let’s dive in!

Liskov Substitution Principle: The L in SOLID

The Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. In other words, if class B is a subclass of class A, then we should be able to substitute A with B without unexpected consequences or errors.

The principle is named after Barbara Liskov, who first introduced it in a conference paper on data abstraction. LSP is crucial for creating robust and maintainable software systems. It keeps your inheritance hierarchy logical, ensuring that derived classes extend functionality in consistent, compatible ways.

Key Ideas Behind LSP

  1. Behavioral Compatibility: Subclasses must not violate the properties and behavior established by the superclass.
  2. Preconditions and Postconditions: A subclass should not strengthen the superclass’s preconditions, nor weaken its postconditions.
  3. No Unexpected Exceptions: A subclass should not throw unexpected exceptions that the superclass does not handle.
  4. No Narrowing of Input Ranges: A subclass should not narrow down input parameters (e.g., via stricter validations) if the base class was more flexible.

By following LSP, you create a clear and consistent inheritance structure, making your codebase easier to understand, extend, and maintain.


Simple Bird Example

A classic, simpler example is the “bird” scenario, which highlights how LSP can be violated if we assume all birds behave identically (e.g., flying).

Violation: Forcing All Birds to Fly

// ❌ BAD: Forcing every Bird to fly
class Bird {
  fly(): string {
    return 'I am flying!'
  }
}

class Eagle extends Bird {
  // This is fine, eagles can fly
}

class Penguin extends Bird {
  // Penguins can't fly, forced to override or throw an error
  fly(): string {
    throw new Error('Penguins cannot fly!')
  }
}

function testFlying(bird: Bird) {
  console.log(bird.fly())
}

// Testing:
const eagle = new Eagle()
testFlying(eagle) // "I am flying!"

const penguin = new Penguin()
testFlying(penguin)
// ❌ Throws error - code expected a normal fly() method

Here, Penguin violates LSP because we are substituting a Bird with a subclass that can’t fulfill the fly() contract. The original Bird class expects all birds to fly, so this leads to broken expectations in testFlying().

Fixing the Violation

Option A: Separate Flyable vs. Non-Flyable

interface IFlyableBird {
  fly(): string
}

class Eagle implements IFlyableBird {
  fly(): string {
    return 'Soaring high!'
  }
}

class Penguin {
  swim(): string {
    return 'Swimming along!'
  }
}

Now, we don’t assume all birds must implement fly(). We’ve split “bird-ness” into more specific behaviors, so we don’t break LSP.

Option B: Use Composition

If you want to introduce different capabilities (flying, swimming, etc.), define them as separate small interfaces or classes and compose them where needed instead of forcing every bird to implement everything.


LSP Example in React

Even though React rarely involves class inheritance directly, you could still see LSP-like concerns in specialized components or shared hooks.

Button Variations

Imagine you have a base Button component and then create a LinkButton. If LinkButton extends the same props and behaves consistently, it’s good. If it changes how those props work (e.g., ignoring onClick or expecting brand-new required props), it could violate LSP.

type ButtonProps = {
  onClick: () => void
  label: string
}

function Button({ onClick, label }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>
}

// ✅ LSP: LinkButton extends the usage without breaking anything
type LinkButtonProps = ButtonProps & { href: string }

function LinkButton({ onClick, label, href }: LinkButtonProps) {
  return (
    <a href={href} onClick={onClick} style={{ textDecoration: 'none' }}>
      {label}
    </a>
  )
}

Here, LinkButton can be substituted anywhere a Button is expected (as long as the caller provides href too, or you make it optional). We don’t break onClick or label usage. If LinkButton started throwing errors unless a new prop was given—while ignoring the original onClick—that would be a violation.


LSP Example in Svelte

Svelte relies on component composition rather than class-based inheritance, but the concept of substitutability can still apply to how you structure components.

Base Alert vs. Dismissible Alert

Let’s assume we have a BaseAlert.svelte and a DismissibleAlert.svelte extending it. I will old, boring Svelte 4 for this example.

// BaseAlert.svelte
<script lang="ts">
  export let message: string;
  export let type: 'info' | 'success' | 'error' = 'info';
</script>

<div class={`alert alert-${type}`}>
  {message}
</div>
// DismissibleAlert.svelte
<script lang="ts">
  import BaseAlert from './BaseAlert.svelte';

  export let message: string;
  export let type: 'info' | 'success' | 'error' = 'info';
  let show = true;

  function dismiss() {
    show = false;
  }
</script>

{#if show}
  <BaseAlert {message} {type} />
  <button on:click={dismiss}>Close</button>
{/if}

You can substitute BaseAlert with DismissibleAlert for a certain portion of your code without breaking expectations: you still display a message and a type, plus you add the dismiss button. If, however, DismissibleAlert changed how type worked or required an additional mandatory prop, it might not be a direct substitute anymore.

Other Real-World Cases

Tracking Components (Analytics/Ecommerce)

You might have an ITracker interface and a BaseTracker that logs page views and clicks. An EcommerceTracker extends it with purchase tracking but keeps the original methods intact.

interface ITracker {
  trackPageView(url: string): void
  trackClick(elementId: string): void
}

class BaseTracker implements ITracker {
  trackPageView(url: string) {
    /* ... */
  }
  trackClick(elementId: string) {
    /* ... */
  }
}

// ✅ LSP: extends the interface without breaking it
class EcommerceTracker implements ITracker {
  trackPageView(url: string) {
    /* ... */
  }
  trackClick(elementId: string) {
    /* ... */
  }
  trackPurchase(orderId: string, amount: number) {
    /* ... */
  }
}

As long as EcommerceTracker doesn’t break the original contract (e.g., doesn’t throw errors if the data is missing), any code that expects an ITracker can swap in an EcommerceTracker safely.

Base Modal vs. Specialized Modals

You might have a BaseModal that respects an isOpen prop and a ClosableModal that extends it with a close button. If ClosableModal still responds to isOpen the same way, you preserve LSP. If it changed the meaning of isOpen or broke how it toggles visibility, you’d violate LSP.

// BaseModal.svelte
<script>
  export let isOpen = false;
</script>

{#if isOpen}
  <div class="modal-backdrop">
    <div class="modal-content">
      <slot />
    </div>
  </div>
{/if}
// ClosableModal.svelte
<script>
  import BaseModal from './BaseModal.svelte';
  export let isOpen = false;

  function handleClose() {
    isOpen = false; // or emit an event
  }
</script>

<BaseModal {isOpen}>
  <button on:click={handleClose}>Close</button>
  <slot />
</BaseModal>

Fetch/HTTP Clients

You may start with a BaseHttpClient that handles generic fetch logic. Then you create an AuthenticatedHttpClient adding auth headers. If the subclass keeps the same method signatures and return types, it’s LSP-compliant. If it changes the data shape or throws unexpected errors, that breaks existing expectations and violates LSP.

interface IHttpClient {
  get<T>(url: string): Promise<T>
  post<T>(url: string, body: any): Promise<T>
  // etc.
}

class BaseHttpClient implements IHttpClient {
  async get<T>(url: string): Promise<T> {
    const response = await fetch(url)
    return response.json()
  }

  async post<T>(url: string, body: any): Promise<T> {
    const response = await fetch(url, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: { 'Content-Type': 'application/json' },
    })
    return response.json()
  }
}

class AuthenticatedHttpClient implements IHttpClient {
  constructor(private token: string) {}

  async get<T>(url: string): Promise<T> {
    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${this.token}` },
    })
    return response.json()
  }

  async post<T>(url: string, body: any): Promise<T> {
    const response = await fetch(url, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${this.token}`,
      },
    })
    return response.json()
  }
}

Conclusion

The Liskov Substitution Principle keeps inheritance or extended components logical and consistent. If a subclass or specialized component breaks the “contract” of its base (e.g., the expected methods and behavior), it causes unexpected results or errors.

By following LSP, your derived classes (or specialized components) will integrate seamlessly wherever the base classes (or base components) are expected. This ensures your code remains reliable, testable, and consistent—making refactoring and future enhancements much smoother.

Stay tuned for the next article in this SOLID series, where we’ll delve into the Interface Segregation Principle (the I in SOLID).

Here are other articles that might be interesting for you
Next.js Banner Image
Do you want to prototype your ideas faster?

Check out my latest Nextjs 15 template! Done with motion, shadcn, support for several languages, newsletter, etc!

See more

FAQ about Liskov Substitution Principle


Share article