When building scalable applications in Salesforce Apex, we often use inheritance, abstract classes, and interfaces to create configurable, maintainable and reusable code. These features help us design systems that are easy to maintain and extend. However, if they are used incorrectly, they can introduce subtle bugs that are difficult to detect and fix. Liskov Substitution Principle (LSP) is the most important object-oriented design principle that helps prevent these issues.
In this guide, we’ll explain LSP in simple, practical terms so that it is easy to understand and remember.
Refer post – Understanding Liskov Substitution Principle
What is the Liskov Substitution Principle?
The Liskov Substitution Principle states that if a class extends another class or implements an interface, it should be able to replace the parent type without changing the expected behavior of the application.
In simple terms:
Subtypes must be substitutable for their base types
This means that code written to use a base class should not need to know which specific subclass it is working with. Every implementation should behave consistently and reliably.
Why LSP Matters in Apex
In Apex, developers frequently use patterns such as:
- Service interfaces
- Trigger handler frameworks
- Payment processors
- Notification services
- Data export utilities
These designs rely on polymorphism, where different classes can be used interchangeably through a common interface. If one implementation behaves differently than expected—for example, by throwing an exception for a method it does not support—it breaks the contract and can cause failures in production.
Classic LSP Violation in Apex
We need to implement the notification service class that
- sends notifications to the user
- get delivery notification
- retry notification service
These notifications can be of different types, such as email, SMS, Slack, etc. We will create a NotificationSender class to send notifications.
Create an EmailSender class that sends a notification using email. It will extends NotificationSender class.
Create an InAppNotificationSender class that sends a notification using email. It will extend the NotificationSender class.
When any code that calls getDeliveryId() on a NotificationSender reference will get null when the actual object is an InAppNotificationSender — silently producing wrong results. Any code that calls retry() will throw at runtime. The substitution is broken. This is a violation of the Liskov substitution principle.
Diagnosing the Real Problem
The real problem with the above violation is a design problem. An abstract class was built for EmailSender capabilities, and then it was forced onto InAppNotificationSender. NotificationSender was supposed to send a notification (all senders can do this), and later added track and retry (only some senders can do this).
LSP Fix: Interface Design
To fix the design problem, we will create two separate interfaces, one for sending notifications (INotificationSender) and the other for retry and delivery notifications(ITrackableNotificationSender)
The EmailSender class will implement ITrackableNotificationSender. The class has provided implementations of all three methods.
The InAppNotificationSender class will implement INotificationSender. The class has provided implementations of a single method of the interface.
The SMSSender class will implement ITrackableNotificationSender. The class has provided implementations of all three methods.
Now the contracts are honoured:
- Code that only needs to send notifications uses
INotificationSender - Code that needs to track delivery uses
ITrackableNotificationSender
Benefits of LSP in Apex
1. Reliable Polymorphism
When a class depends on an INotificationSender, it can confidently work with any implementation of that interface. There is no need for instanceof checks, extra null handling, or concern about unexpected runtime failures.
2. Safe Dependency Injection in Tests
LSP is the foundation that makes mock objects effective. If MockNotificationSender correctly implements the INotificationSender contract, it can be injected anywhere the interface is expected and will behave consistently during unit tests.
3. Less Defensive Coding
When all implementations honor the same contract, consuming code remains clean and straightforward. We do not need conditional logic such as if (sender instanceof EmailSender) because the interface itself guarantees the required behavior.
4. Confident Refactoring and Replacement
If each implementation fully satisfies the contract, one implementation can be replaced with another—such as swapping EmailSender for SendGridEmailSender—without affecting the rest of the application. The implementation changes, but the contract remains stable.
Drawbacks of LSP
1. Requires Thoughtful Interface Design Up Front
Liskov Substitution Principle violations often arise when an interface is created around the needs of a single implementation—typically the first one. To satisfy LSP, we must design abstractions that accurately represent the behaviour shared by all current and future implementations. This requires broader architectural thinking at the outset, which can make interface design more challenging.
2. May Lead to Additional Interfaces
When implementations differ in meaningful ways—for example, some objects support tracking while others do not—LSP often reveals that a single interface is too broad. The appropriate solution is to split the abstraction into smaller, more focused interfaces. This aligns closely with the Interface Segregation Principle, and the two principles frequently complement one another.
3. Difficult to Detect Automatically
LSP violations are not typically caught by the compiler. In Apex, a class can technically fulfil an interface contract while returning null or throwing UnsupportedOperationException. Although the code compiles, substituting that implementation can still break client expectations. Detecting these issues requires careful code reviews, strong architectural oversight, and contract-based testing.
When Should We Apply LSP Thinking?
Apply LSP when:
- We are defining an interface or abstract class with multiple implementations.
- We use polymorphism — code that works with a base type and receives different implementations at runtime.
- A class is doing
instanceofchecks on an interface type — this is a red flag that LSP may already be violated. - We are building a plugin or strategy pattern (OCP + LSP go hand-in-hand).
- We are writing mocks for unit testing — a mock is an LSP implementation of the real interface.
When Should We NOT Apply LSP?
Avoid over-applying LSP when:
- We have no polymorphism — we instantiate exactly one concrete class and never pass an abstract type. LSP is about substitutability, which only matters in a polymorphic context.
- We have a helper utility class with static methods — no inheritance, no interfaces, no substitution needed.
- A subclass is explicitly a test double and lives only in test classes (
@isTest) — it won’t reach production code.
Summary
The Liskov Substitution Principle is what makes interfaces and polymorphism in Apex safe and trustworthy. When LSP is honored, we can inject any implementation behind an interface and the system behaves correctly — which is the entire point of abstraction.
The key takeaway is simple: design interfaces around what every implementation can genuinely support, not just what the first implementation happens to do. If certain implementations cannot fulfill part of the contract, the right solution is to split the interface rather than forcing classes to pretend they support behavior they do not.
LSP forms the foundation for dependable mock objects, robust dependency injection, and safe refactoring. Together, these practices create a Salesforce org that is easier to maintain, faster to evolve, and far less prone to unexpected regressions.
Need SOLID implementation: Contact us with your requirements
Other SOLID Principle
References
- The Art of Naming (Clean Code for Salesforce Developers)
- Top Mistakes Developers Make in Salesforce Apex Triggers
- How to Handle Bulkification in Apex with Real-World Use Cases
- How to Confidently Manage Transactions in Salesforce Apex
Other Important Posts
- The Ultimate Guide to Apex Order of Execution for Developers
- How to Manage Technical Debt in Salesforce
- The Ultimate Checklist for Efficiently Querying Large Data Sets
- Optimizing Salesforce Apex Code
- Efficient Ways to Debug Salesforce Platform Events
- Handle Heap Size for Apex Code Optimization
- Build Scalable Solutions with Salesforce
- How to Correctly Publish Platform Event Using Salesforce Apex
