Think of the SOLID principles as five golden rules for crafting code that’s both robust and flexible. Coined by the legendary Robert C. Martin (aka Uncle Bob), these principles are foundational to clean, maintainable object-oriented design:
The Liskov Substitution Principle (LSP) is the unsung hero that ensures substituting one object for another doesn’t break your system. It enforces logical structure and reliability in your code:
Named after computer scientist Barbara Liskov, this principle boils down to a simple but powerful idea: you should be able to replace a parent class with one of its subclasses without breaking your code.
In plain English, if your code works with a general “vehicle” class, it should still work just fine if you swap in a “car” or a “truck” class instead. No bugs, no weird surprises, no system crashes.
For example, imagine software built for a self-driving car that’s designed to handle any vehicle. If everything works fine with a car, but the system breaks when you switch to a truck, then boom, LSP has been violated. The code relied too much on the parent and didn’t account for the behavior of its children.
That’s exactly what LSP helps us avoid:
LSP is not some nerdy rule for software developers; it is the glue that binds your extensible systems together.
Once you design a system that follows the LSP, you’ll enjoy the following benefits:
At its core, the Liskov Substitution Principle is all about trust, you should be able to replace a parent class with a child class and expect everything to still work smoothly. But to really follow LSP, there’s more than just swapping classes involved.
To stay true to the principle, a subclass should not demand more from the code (weakened preconditions) or do less than what was originally promised (strengthened postconditions).
Preconditions are the conditions that need to be true before a method runs. With LSP, a subclass shouldn’t ask for more than what the superclass already requires. In other words, it shouldn’t be pickier or impose stricter rules; that would break the trust your program has in the original class.
Postconditions are the outcomes or guarantees that come after a method finishes. LSP says a subclass shouldn’t deliver less than what the superclass promised. If the parent class guarantees a certain result, the child class has to keep that promise or do even better,but never less.
At first glance, it may seem logical to say that a square is just a rectangle with equal sides, right? So, naturally, you might assume that a square could be used as a subclass of a rectangle. But when you look at this example more closely, it reveals a violation of the principle.
Picture your pizza factory designing pizza boxes. The blueprint for the design calls for a rectangular box, a flexible shape where you can set the width to any size without affecting the height, and vice versa.
In this setup, the precondition is that adjusting one side doesn’t compromise the other. The postcondition is that the resulting area will be a perfect fit for the pizza size.
Now, let’s say you introduce a square box as a subclass, assuming it’s just a special case of a rectangle. But the moment you input values for width and height, the square enforces that both must be equal. So if you set the width to 12 and the height to 8, the square takes the last value, say 8 and uses it for both.
This weakens the precondition and compromises the result. Instead of an area of 12Ă—8 = 96, you now get 8Ă—8 = 64 and the pizza no longer fits.
In a nutshell, when you think of LSP, think of it as the golden rule of inheritance: if you (subclass) are going to inherit a name (superclass), inherit the behavior too.
Even when we understand the Liskov Substitution Principle (LSP), it’s easy to violate it, sometimes without realizing, and sometimes because external factors make it hard to avoid.
Let’s talk about a common scenario where these violations tend to surface.
The square-rectangle example we discussed earlier highlights how subclassing can backfire. While a square is a rectangle mathematically, it behaves differently in practice. A Square
class that enforces equal sides can’t reliably substitute a Rectangle
class that expects width and height to be independent. The result? A broken substitution and unexpected bugs.
Rather than forcing an inheritance relationship where behavior diverges, it’s better to keep these entities separate. Let the square and rectangle exist as independent shapes that share common features, not as strict parent and child classes.
LSP is often violated when a subclass removes or changes a behavior that the superclass originally promised. And when that happens, you usually end up with unexpected exceptions, and equally unexpected bugs.
For example, say you’ve created a digital file system where accessing files is straightforward and hassle-free. Everything works smoothly. Then, you introduce a subclass for secure files that now asks for special permission before access. Makes sense, right?
But here’s where it gets tricky: the rest of the system still expects all files including secure ones to behave like regular files. So when it tries to access a SecureFile
the usual way, things go sideways. The result? Exceptions where there weren’t any before.
A cleaner approach is to gently adjust the subclass behavior while still respecting the “contract” defined by the parent class. This lets you introduce new functionality without completely breaking what the rest of the system expects and without violating the Liskov Substitution Principle.
Another common LSP violation happens when a subclass returns something unexpected, something the system isn’t prepared to handle.
Take a user-retrieval system, for example. It’s built on the assumption that it always returns a valid user object. Then, along comes a new subclass that, when a user isn’t found, simply returns Nothing
. Now, the system tries to treat Nothing
like a real user and promptly crashes.
To avoid this, if there’s a chance the user might not exist, return a placeholder object instead, and send a clear error message alongside it. Alternatively, make sure the subclass still returns something the system expects behavior that’s consistent with the parent class.
No matter what programming language you’re working with, the core idea of the Liskov Substitution Principle (LSP)stays the same: subclasses should be reliable stand-ins for their parent classes. But while the principle doesn’t change, how you apply it can look different depending on the language.
Let’s take a look at how LSP plays out in some of the most commonly used programming languages.
Python has always felt like the friendly neighborhood language, flexible, laid-back, and rarely uptight about strict type rules. But when it comes to the Liskov Substitution Principle (LSP), that flexibility can make things a little tricky.
Because Python relies on duck typing, it cares more about how something behaves than what it’s officially called. If it looks like a duck and quacks like a duck, Python’s all in. The same goes for subclasses, as long as they act like their parent class, Python usually won’t complain.
This makes following LSP in Python less about rigid structure and more about keeping behavior consistent. It’s powerful, but it also means you’ve got to be careful, silent violations can slip through if you’re not paying attention.
Python also gives you a way to be a bit more strict when needed, through Abstract Base Classes (ABCs). Think of them as blueprints that help enforce structure. By using ABCs, you can make sure that any subclass sticks to the rules set by its parent by requiring it to implement specific methods:
from abc import ABC, abstractmethod class Bird(ABC): @abstractmethod def fly(self) -> None: pass class Sparrow(Bird): def fly(self) -> None: print("Sparrow flying") # satisfies the contract # Client code def let_it_fly(b: Bird): b.fly()
By programming to an abstract base, any subclass supporting fly()
can be used interchangeably.
Java is the serious, no-nonsense architect of the programming world built on a statically typed system and known for its love of rigid class hierarchies. It’s structured, predictable, and everything has its place.
That’s exactly why the Liskov Substitution Principle (LSP) matters so much here. Just like building a skyscraper, you need every part to fit without compromising the foundation. LSP ensures that subclasses can slot into these carefully crafted hierarchies without toppling the whole system. If you replace a parent class with a child class, your application should still run smoothly, no exceptions, no surprises.
But here’s the catch: Java’s inheritance model can be a bit fragile. It’s easy to violate LSP if you’re not careful, especially with deep inheritance chains. One subclass behaving slightly differently can cause the whole structure to crack.
The fix? Keep your inheritance chains short and logical. And when in doubt, prefer composition over inheritance. It’s a cleaner, safer way to build behavior while staying true to LSP:
interface Shape { double area(); } class Circle implements Shape { private final double radius; public Circle(double r) { radius = r; } public double area() { return Math.PI * radius * radius; } }
Any subclass implementing area()
should respect the original contract, no unexpected side effects, and the result should always be a positive value.
Given that in TypeScript, React components are king, your components must adhere to LSP to prevent disruption in the flow of your system. For instance, if you are building a component-based system, you’ll want each component to respect the substitution principle by being interchangeable. While a superclass might allow for a generic component, the subclass should not break the parent’s expectations.
TypeScript enforces this structure through its static typing system, ensuring that subclasses maintain consistent method signatures and don’t introduce unexpected components by strictly defining props
and state
interfaces:
interface ButtonProps { onClick: () => void; label: string; } function PrimaryButton(props: ButtonProps) { return <button onClick={props.onClick}>{props.label}</button>; } function IconButton(props: ButtonProps & { icon: string }) { return <button onClick={props.onClick}> <i className={props.icon} /> {props.label} </button>; }
IconButton
extends the props without removing or altering any of the required fields.
C# uses abstract classes and interfaces to enforce order and structure, a big win in systems where the Liskov Substitution Principle (LSP) is key to maintainability. Interfaces help define a clear contract for subclasses to follow, ensuring their behavior stays consistent with expectations. This becomes especially important in enterprise applications, where reliability isn’t optional, it’s essential:
public interface IRepository<T> { T GetById(int id); } public class SqlRepository<T> : IRepository<T> { public T GetById(int id) { // fetch from SQL DB } }
Concrete repositories must uphold guarantees around non-null returns and proper exception handling.
The Liskov Substitution Principle continues to prove its value in modern systems by laying the groundwork for complex architectures to function reliably. Let’s take a closer look.
When you think of LSP in functional programming, think of it as the principle that maintains the purity of your functions in a system. The output of functions in functional programming is expected to rely entirely on their input. LSP in this environment ensures that functions swapped with another function, possibly a higher-order function, do not introduce unexpected behavioral surprises.
Similarly, you need the help of LSP for software created using dependency injection (DI) to be easily replaceable, scalable, and fixable. The primary role of the principle is to allow for the substitution of a service without a hitch.
In DI, you inject dependencies into your classes instead of hard-coding them. When a service is injected into a class, LSP ensures that it can be interchanged with a different version without disrupting the system.
When you design React components, the superclass should behave and function just as well as the subclass. At the core, this means your components should respect the same interface. In large React applications where different parts of the app rely on consistent state
and props
this becomes especially important. Applying LSP in this context helps maintain the consistency needed to keep things running smoothly.
The role of LSP in Angular services is no different. It follows the same logic: services should be swappable without breaking the system. This is especially valuable in complex Angular applications, where services are injected into multiple components. LSP ensures those services remain consistent and predictable, regardless of which version is being used:
Angular services: @Injectable({ providedIn: 'root' }) export abstract class AuthService { abstract login(credentials: Credential): Observable<User>; } @Injectable() export class RealAuthService extends AuthService { login(c: Credential) { /* http call */ } }
Clients should be able to swap between mock and real implementations without changing the expected behavior.
LSP is your go-to principle when it comes to maintaining API compatibility as services evolve in a microservices setup. By following LSP, you can replace an old version of an API with a new one, without breaking existing functionality because the original contracts still hold.
It’s the perfect recipe for smooth transitions between different service versions, ensuring that changes don’t ripple out and disrupt other parts of the system.
There’s a connection between each of the SOLID principles, and the Liskov Substitution Principle (LSP) plays a key role in that relationship. Here are a few ways LSP intersects with the other principles:
You can only confidently extend a system using the OCP approach if you’re sure that your subclasses will stick to the expected behavior. If a new subclass changes how it behaves or alters the input/output format, it doesn’t just break LSP, it also weakens OCP. In that way, LSP acts like the bodyguard, making sure your system stays safe and stable as it grows.
When it comes to the Single Responsibility Principle (SRP), LSP helps keep the inheritance chain from becoming fragile, while SRP ensures classes stay clean and focused. Without SRP, a parent class can easily drift into doing too much, bringing in unexpected behaviors that don’t belong together. That mess then gets passed down to the subclasses, forcing them to inherit responsibilities they shouldn’t, which leads to leaky abstractions.
This is where LSP steps in making sure subclasses inherit only the behavior that’s actually relevant, not everything the superclass happens to include.
Here are a few recommended best practices to help you apply the Liskov Substitution Principle effectively:
Here are some helpful guidelines for refactoring code that violates the Liskov Substitution Principle:
To structure tests that follow the Liskov Substitution Principle:
This approach helps ensure your subclasses truly behave as drop-in replacements.
LSP is often violated when inheritance is used inappropriately. As fundamental as inheritance might seem, it’s not always the right tool for the job. In many cases, composition building behavior by combining smaller, focused parts instead of extending from a single ancestor, can be a cleaner, more reliable alternative.
The Liskov Substitution Principle (LSP) may sound like a brainy, textbook concept but it’s a crucial layer in writing code that works. At its heart, LSP ensures that subclasses can seamlessly stand in for their superclasses without causing unexpected issues.
Of course, LSP doesn’t stand alone. It works hand-in-hand with other SOLID principles like SRP and OCP. Together, they help you build applications that are modular, scalable, and easy to test. OCP allows you to extend your code without rewriting what’s already there, while SRP keeps each class focused and manageable.
As a rule of thumb: if your subclass can confidently take the place of its superclass without breaking anything, you’re on the right track with LSP.
LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.
With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.
Modernize how you understand your web and mobile apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThis article walks through new CSS features like ::scroll-button()
and ::scroll-marker()
that make it possible to build fully functional CSS-only carousels.
Let’s talk about one of the greatest problems in software development: nascent developers bouncing off grouchy superiors into the arms of AI.
Flexbox and Grid are the heart of modern CSS layouts. Learn when to use each and how they help build flexible, responsive web designs — no more hacks or guesswork.
Responsive design is evolving. This guide covers media queries, container queries, and fluid design techniques to help your layouts adapt naturally to any screen size.