1. Introduction
The Liskov Substitution Principle (LSP) is one of the SOLID design principles used to keep inheritance and polymorphism safe and predictable.
In practical Java development, LSP means that if a class extends another class or implements an interface, the new implementation should behave in a way that existing callers can safely use without needing special checks.
A subtype should not surprise the code that already works with its parent type.
This becomes especially important in:
- Spring Boot service implementations
- Payment integrations
- Notification providers
- Repository abstractions
- File-storage providers
- Authentication mechanisms
- Pricing strategies
- External API clients
- Business-rule implementations
A common code-review warning sign is inheritance that is technically valid according to the Java compiler but logically invalid according to the business contract.
2. What This Topic Means
Suppose an application contains an abstraction:
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
}Different implementations may exist:
public class CardPaymentProcessor implements PaymentProcessor {
public PaymentResult process(PaymentRequest request) {
// Process card payment
return new PaymentResult(true);
}
}
public class UpiPaymentProcessor implements PaymentProcessor {
public PaymentResult process(PaymentRequest request) {
// Process UPI payment
return new PaymentResult(true);
}
}Code using PaymentProcessor should work correctly regardless of whether it receives the card implementation or UPI implementation.
That is the practical meaning of substitutability.
The caller should not need code such as:
if (processor instanceof CardPaymentProcessor) {
...
} else if (processor instanceof UpiPaymentProcessor) {
...
}A subtype violates LSP when it changes the expected contract in a way that makes normal parent-type usage unsafe.
Typical violations include:
- Throwing
UnsupportedOperationExceptionfor an inherited operation. - Accepting fewer valid inputs than the parent contract.
- Returning unexpected values such as
null. - Changing important business semantics.
- Requiring callers to know the concrete implementation.
- Breaking guarantees documented by the abstraction.
- Introducing stronger runtime restrictions.
3. Why It Matters in Real Projects
LSP directly affects maintainability, reliability, testability, and team development.
Maintainability
Developers should be able to introduce a new implementation without modifying every caller.
Without LSP, adding another implementation often creates:
if (type == ...)
if (service instanceof ...)
switch (providerType)throughout the application.
Reliability
A class may compile correctly while violating assumptions made by the caller.
This creates runtime failures that are difficult to identify during compilation.
Debugging
When implementations behave inconsistently, developers have to determine which concrete implementation was injected before understanding a failure.
Testability
Proper abstractions make implementations replaceable by mocks, stubs, or test implementations.
Team Development
In large teams, different developers implement the same interfaces. A stable behavioral contract prevents one implementation from introducing unexpected behavior.
4. Core Concept
LSP can be summarized practically as:
Code written against an abstraction should continue to behave correctly when any valid implementation of that abstraction is supplied.
Suppose:
public interface NotificationSender {
void send(Notification notification);
}The application assumes that calling send() sends the supplied notification or reports a meaningful failure.
Now imagine:
public class SmsNotificationSender implements NotificationSender {
public void send(Notification notification) {
throw new UnsupportedOperationException("SMS temporarily unsupported");
}
}Technically, this class implements the interface.
Architecturally, it may be incorrect.
If the application treats every NotificationSender as capable of sending notifications, this implementation breaks the abstraction's contract.
LSP Is About Behavior
LSP is not simply:
Child class extends parent class.
The important question is:
Can the child safely be used wherever the parent is expected?
Reviewers should therefore examine behavioral compatibility rather than inheritance syntax alone.
5. Important Rules
When reviewing Java inheritance or interface implementations, follow these practical rules.
- A subtype should preserve the meaningful contract of its parent.
- Do not override a method only to disable it.
- Avoid returning
nullwhere the abstraction promises a valid result. - Do not introduce unexpected exceptions for valid parent-level inputs.
- Do not silently change business semantics.
- Avoid requiring callers to check the concrete subtype.
- Do not use inheritance merely for code reuse.
- Prefer composition when two classes do not truly share the same behavioral contract.
- Keep interface contracts small and focused.
- Separate optional capabilities into separate interfaces.
- Document important preconditions and postconditions.
- Ensure every implementation respects those contracts.
- Add contract tests when many implementations share the same abstraction.
6. Bad Code Example
Consider an e-commerce system supporting different payment methods.
The development team defines:
public abstract class PaymentProcessor {
public abstract PaymentResult process(PaymentRequest request);
public abstract void refund(String transactionId);
}The credit-card implementation supports both operations:
public class CardPaymentProcessor extends PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
return new PaymentResult(true, "CARD_PAYMENT_COMPLETED");
}
@Override
public void refund(String transactionId) {
System.out.println("Card refund completed: " + transactionId);
}
}Later, another developer creates:
public class BankTransferPaymentProcessor extends PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
return new PaymentResult(true, "BANK_TRANSFER_CREATED");
}
@Override
public void refund(String transactionId) {
throw new UnsupportedOperationException("Bank transfer cannot be refunded");
}
}A service uses the abstraction:
@Service
public class RefundService {
public void refund(PaymentProcessor paymentProcessor, String transactionId) {
paymentProcessor.refund(transactionId);
}
}The compiler accepts this design.
But RefundService cannot actually work safely with every PaymentProcessor.
That indicates an LSP violation.
7. Problems in the Bad Code
Incorrect Abstraction
PaymentProcessor claims that every payment processor supports refunds.
That business assumption is false.
UnsupportedOperationException
The child implementation inherits an operation that it cannot honor.
This is one of the strongest practical indicators of an LSP problem.
Runtime Failure
The following code is valid according to the type system:
PaymentProcessor processor = new BankTransferPaymentProcessor();
processor.refund("TXN-1001");But it fails at runtime.
Caller Knowledge Leakage
Developers may eventually introduce:
if (!(paymentProcessor instanceof BankTransferPaymentProcessor)) {
paymentProcessor.refund(transactionId);
}The caller now needs knowledge of implementation details.
Maintainability Problem
Every new payment mechanism may require more subtype-specific conditions.
Production Risk
A refund workflow may fail only for certain payment types after deployment.
8. Code Review Findings
A senior reviewer should notice several issues.
Finding 1
The parent abstraction requires all payment processors to support refunds, but the bank-transfer implementation does not support that capability.
Finding 2
UnsupportedOperationException indicates that the subtype cannot satisfy the parent's behavioral contract.
Finding 3
Calling code assumes that every PaymentProcessor is refundable.
Finding 4
The abstraction combines two different capabilities:
- Processing payments
- Refunding payments
Not every payment mechanism necessarily supports both.
Finding 5
The design will probably produce instanceof, provider-type switches, or special conditions as additional implementations are introduced.
9. Reviewer Comment Example
A practical PR comment could be:
BankTransferPaymentProcessorcannot satisfy therefund()contract and therefore is not safely substitutable forPaymentProcessor. Consider separating refund capability from payment processing instead of throwingUnsupportedOperationException.
Another acceptable comment:
Since refunds are not supported by every payment method,
refund()should not be part of the common processor abstraction. A separate refundable-payment contract would make unsupported operations impossible through the type system.
10. Improved Code
Separate the capabilities.
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
}Create a dedicated contract for refundable payment mechanisms.
public interface RefundablePaymentProcessor extends PaymentProcessor {
RefundResult refund(String transactionId);
}Card payments support both:
@Component
public class CardPaymentProcessor implements RefundablePaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
return new PaymentResult(true, "CARD_PAYMENT_COMPLETED");
}
@Override
public RefundResult refund(String transactionId) {
return new RefundResult(true, transactionId);
}
}Bank transfer supports payment processing only:
@Component
public class BankTransferPaymentProcessor implements PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
return new PaymentResult(true, "BANK_TRANSFER_CREATED");
}
}The refund service accepts only implementations that actually support refunds:
@Service
public class RefundService {
public RefundResult refund(RefundablePaymentProcessor paymentProcessor, String transactionId) {
return paymentProcessor.refund(transactionId);
}
}The API now expresses the business capability correctly.
11. Improved Code Explanation
Payment Processing Remains Generic
Every payment implementation can implement:
PaymentProcessorbecause all implementations genuinely support process().
Refund Is an Explicit Capability
Only implementations supporting refunds implement:
RefundablePaymentProcessorUnsupported Operations Disappear
There is no reason to write:
throw new UnsupportedOperationException(...);Invalid Usage Becomes Harder
RefundService requires:
RefundablePaymentProcessorTherefore, developers cannot accidentally pass a normal non-refundable PaymentProcessor.
Caller Logic Becomes Simpler
There is no need for:
instanceof
provider-type checks
exception-based capability detectionThe Java type system now represents the business rules.
12. Bad Code vs Improved Code
| Area | Bad Design | Improved Design |
|---|---|---|
| Abstraction | Assumes every payment supports refund | Models refund as separate capability |
| Runtime safety | Unsupported operations fail at runtime | Unsupported usage is prevented structurally |
| Readability | Business capability is unclear | Capability is obvious from interface |
| Maintainability | New processors may require conditions | New processors implement only applicable contracts |
| Testability | Tests must handle unsupported behavior | Contracts can be tested independently |
| Reliability | Valid-looking calls may fail | Valid interface calls have meaningful behavior |
13. Real Project Scenario
Consider a financial platform integrating several transaction providers.
Supported payment methods include:
- Credit cards
- UPI
- Bank transfer
- Wallet
- Cash on delivery
The original architecture defines:
interface PaymentGateway {
pay();
refund();
cancel();
capture();
}But capabilities differ.
A credit-card authorization may support:
pay
refund
cancel
captureA bank transfer may support:
payA wallet may support:
pay
refundCash on delivery may follow an entirely different lifecycle.
If every provider implements one large interface, developers start throwing unsupported-operation exceptions.
Eventually application services contain logic such as:
switch (paymentMethod) {
case CARD:
...
case UPI:
...
case BANK_TRANSFER:
...
}The abstraction has stopped providing useful polymorphism.
A better design models actual capabilities independently.
14. Production Impact
An LSP violation can create serious production issues.
Unexpected Runtime Exceptions
A valid interface invocation may unexpectedly throw:
UnsupportedOperationException
IllegalStateException
NullPointerExceptionIncorrect Business Workflows
An order service may believe a cancellation or refund succeeded when an implementation silently ignores it.
Difficult Incident Diagnosis
Failures may occur only with specific implementations or providers.
For example:
Card -> success
Wallet -> success
Bank transfer -> failureGrowing Conditional Logic
Teams may patch the architecture using subtype-specific checks instead of correcting the abstraction.
Integration Failures
External providers frequently expose different capabilities, making poor inheritance especially risky in integration-heavy systems.
15. Common Developer Mistakes
Using Inheritance Only for Code Reuse
Developers sometimes extend a class because it already contains convenient methods.
Shared implementation does not automatically imply a valid subtype relationship.
Overly Broad Interfaces
Example:
interface StorageService {
upload();
download();
delete();
generatePublicUrl();
archive();
}Not every storage implementation may support every operation.
Throwing UnsupportedOperationException
This often indicates that the subtype does not truly satisfy the parent contract.
Returning Null Instead of Honoring the Contract
Example:
@Override
public Receipt generateReceipt(Order order) {
return null;
}If callers expect a receipt, substitutability is broken.
Adding Subtype Checks
Example:
if (processor instanceof SpecialProcessor) {
...
}Frequent subtype branching suggests that polymorphism is not working properly.
Strengthening Validation Unexpectedly
Suppose the parent accepts all positive amounts.
A subtype accepts only amounts above ₹500.
The subtype has introduced a stronger precondition.
Changing Semantic Meaning
An implementation may return true merely because the request was queued while another returns true only after successful completion.
Both compile, but their behavioral contracts differ.
16. Edge Cases
Null Input
All implementations should follow a consistent contract regarding null.
Avoid:
Implementation A -> IllegalArgumentException
Implementation B -> returns null
Implementation C -> NullPointerExceptionunless such differences are intentionally defined.
Empty Input
An interface accepting collections should clearly define behavior for empty collections.
Boundary Values
If the parent supports:
amount > 0a subtype should not unexpectedly reject valid amounts within that range.
External Provider Failure
Implementations should translate external failures consistently when callers depend on a common abstraction.
Unsupported Business Capability
Do not represent unsupported capabilities through methods that fail at runtime.
Model them through appropriate interfaces or composition.
State-Dependent Operations
If behavior is available only in certain states, make those state requirements explicit.
17. Performance Considerations
LSP is primarily a design and behavioral correctness principle rather than a performance optimization.
However, implementation behavior can still affect performance expectations.
Suppose:
interface CustomerRepository {
Optional<Customer> findById(Long id);
}One implementation performs one indexed SQL query.
Another unexpectedly:
- Loads every customer.
- Filters the list in Java.
- Makes three remote service calls.
Although performance alone does not automatically establish an LSP violation, a subtype that violates important documented operational expectations can undermine the abstraction.
Reviewers should therefore consider whether implementations preserve important characteristics such as:
- Expected database access pattern
- Blocking versus non-blocking behavior
- Expensive remote calls
- Memory usage
- Transaction behavior
Do not claim LSP violation merely because one implementation is slightly slower.
Focus on behavior significant enough to break caller assumptions.
18. Security Considerations
LSP itself is not a security mechanism.
However, substitutable implementations must not unexpectedly weaken security guarantees defined by an abstraction.
Consider:
public interface DocumentService {
Document getDocument(String documentId, UserContext user);
}Suppose the normal implementation verifies authorization.
A new implementation:
public Document getDocument(String documentId, UserContext user) {
return repository.findById(documentId).orElseThrow();
}If authorization is part of the abstraction's required contract, the new implementation violates it.
Potential risks include:
- Authorization bypass
- Sensitive-data exposure
- Inconsistent validation
- Unsafe logging
- Missing tenant isolation
Security-critical guarantees should be explicit, centrally enforced where appropriate, and covered by tests.
19. Testing Considerations
LSP-heavy designs benefit from contract testing.
Suppose several implementations implement:
public interface DiscountCalculator {
Money calculate(Order order);
}Create shared tests that every implementation must satisfy.
Important tests include:
Positive Case
Valid inputs produce a valid result.
Invalid Input
Implementations follow the documented validation contract.
Boundary Case
Minimum and maximum supported business values behave consistently.
Exception Case
Expected failures are represented consistently.
Null Behavior
Verify explicitly if null is allowed or rejected.
Integration Tests
For external implementations, verify that provider-specific responses are translated into the common application contract.
Contract Test Concept
Instead of testing only implementation details, verify:
Does every implementation satisfy what callers of this abstraction are promised?
20. Refactoring Guidelines
When correcting an LSP violation in existing production code, avoid large unsafe rewrites.
Step 1: Identify the Real Contract
Find:
- Parent classes
- Interfaces
- Implementations
- Call sites
- Existing tests
Determine what callers actually expect.
Step 2: Find Unsupported Methods
Search for:
UnsupportedOperationException
return null
empty implementation
no-op methods
instanceofThese often reveal incorrect abstractions.
Step 3: Separate Capabilities
Create narrower interfaces where necessary.
For example:
PaymentProcessor
RefundablePaymentProcessor
CancellablePaymentProcessorStep 4: Preserve Existing Behavior
Add characterization tests before refactoring risky business logic.
Step 5: Migrate Callers Gradually
Change consumers to depend on the narrowest abstraction they genuinely need.
Step 6: Remove Obsolete Methods
After all consumers migrate, remove the invalid parent-level contract.
21. Best Practices
- Model real business capabilities rather than artificial inheritance hierarchies.
- Prefer small interfaces with coherent contracts.
- Use interfaces to express capabilities explicitly.
- Keep parent contracts predictable.
- Document important behavioral guarantees.
- Ensure exceptions have consistent meaning.
- Avoid subtype-specific logic in callers.
- Test multiple implementations against common contract tests.
- Prefer composition when inheritance would create unsupported behavior.
- Use meaningful domain abstractions.
- Make invalid combinations difficult to represent.
- Depend on the narrowest interface required by the consumer.
22. Practices to Avoid
Unsupported Methods
Avoid:
@Override
public void refund() {
throw new UnsupportedOperationException();
}The abstraction probably contains a capability that the subtype does not possess.
Empty Overrides
Avoid:
@Override
public void cancel() {
}Silent no-op implementations are often worse than exceptions because callers may assume the operation succeeded.
Subtype-Specific Conditions Everywhere
Avoid:
if (service instanceof A) {
...
} else if (service instanceof B) {
...
}This defeats the purpose of polymorphism.
Returning Fake Success
Avoid:
return true;when no operation actually occurred.
Inheritance for Shared Utilities
Do not make one domain component a subtype of another merely to reuse helper methods.
Extract shared functionality into composition or utility abstractions instead.
Giant Interfaces
Avoid interfaces containing unrelated optional capabilities.
23. Code Review Checklist
Use these questions during Pull Request review:
- Can every subtype safely be used wherever the parent abstraction is expected?
- Does any implementation throw
UnsupportedOperationExceptionfor a parent method? - Does any overridden method silently do nothing?
- Does any subtype return
nullwhere callers expect a meaningful result? - Has the subtype introduced stronger input restrictions?
- Has the subtype weakened guarantees provided by the parent?
- Are exceptions consistent with the documented contract?
- Does calling code need
instanceofchecks? - Does calling code switch behavior according to concrete implementation type?
- Is inheritance being used merely for code reuse?
- Does the parent abstraction contain operations that only some children support?
- Should optional capabilities be represented through separate interfaces?
- Are important behavioral guarantees covered by tests?
- Can a new implementation be introduced without changing existing consumers?
- Does each implementation preserve important security guarantees?
- Are business semantics consistent across implementations?
24. Common Pull Request Review Comments
- *This implementation throws UnsupportedOperationException for an operation required by the parent contract. Can we separate this capability into a dedicated interface?*
- *The caller now checks the concrete processor type before invoking the method. This suggests the implementations are not fully substitutable. Can we move this behavior behind an appropriate abstraction?*
- *The parent contract accepts all positive amounts, but this implementation rejects values below 500. Please confirm whether this stronger precondition belongs in the shared contract.*
- *Returning null here changes the expected behavior of this interface and forces implementation-specific null handling on callers. Can we preserve the existing contract?*
- *This override is intentionally empty. If this implementation does not support cancellation, cancellation probably should not be part of its interface.*
- *Can we split PaymentProcessor and RefundablePaymentProcessor so unsupported refund operations cannot be called?*
- *This implementation bypasses the authorization validation performed by the other implementations. The security guarantees of the abstraction should remain consistent.*
- *We now have instanceof checks for three implementations. That is likely to become harder to maintain as additional providers are introduced.*
- *Please add contract tests that run against all implementations of this interface.*
- *The subtype changes the meaning of a successful result from "completed" to "queued." We should make these states explicit instead of returning the same success value.*
25. Code Review Exercise
Review the following notification implementation.
public interface NotificationService {
void send(String recipient, String message);
void schedule(String recipient, String message, LocalDateTime sendAt);
}
@Service
public class EmailNotificationService implements NotificationService {
@Override
public void send(String recipient, String message) {
System.out.println("Email sent to " + recipient);
}
@Override
public void schedule(String recipient, String message, LocalDateTime sendAt) {
System.out.println("Email scheduled for " + sendAt);
}
}
@Service
public class SmsNotificationService implements NotificationService {
@Override
public void send(String recipient, String message) {
if (message.length() > 160) {
throw new IllegalArgumentException("SMS cannot exceed 160 characters");
}
System.out.println("SMS sent to " + recipient);
}
@Override
public void schedule(String recipient, String message, LocalDateTime sendAt) {
throw new UnsupportedOperationException("SMS scheduling is not supported");
}
}
@Service
public class CampaignService {
public void scheduleCampaign(NotificationService notificationService, Campaign campaign) {
if (notificationService instanceof SmsNotificationService) {
notificationService.send(campaign.getRecipient(), campaign.getMessage());
return;
}
notificationService.schedule(
campaign.getRecipient(),
campaign.getMessage(),
campaign.getSendAt()
);
}
}Identify:
- Problems
- Code smells
- LSP violations
- Runtime risks
- Maintainability risks
- Improvements
Do not look at the solution until you complete your own review.
26. Exercise Solution
There are several problems in the implementation.
Problem 1: Scheduling Is Not Universally Supported
NotificationService promises:
schedule(...)but SmsNotificationService cannot honor that contract.
This is the primary LSP issue.
Problem 2: UnsupportedOperationException
The SMS implementation fails for a method that callers are allowed to invoke through the parent abstraction.
Problem 3: instanceof in CampaignService
The caller understands internal subtype capabilities:
notificationService instanceof SmsNotificationServiceThis leaks implementation details.
Problem 4: Campaign Semantics Change
For email, a campaign is scheduled.
For SMS, it is immediately sent.
These are different business operations.
Silently replacing scheduling with immediate sending is dangerous.
Problem 5: SMS-Specific Length Constraint
A 160-character restriction may be legitimate for the SMS implementation.
However, the architecture should clearly handle channel-specific message constraints rather than allowing unexpected runtime behavior deep inside the implementation.
Improved Design
Separate sending and scheduling capabilities.
public interface NotificationSender {
void send(String recipient, String message);
}
public interface ScheduledNotificationSender extends NotificationSender {
void schedule(String recipient, String message, LocalDateTime sendAt);
}Email supports scheduling:
@Service
public class EmailNotificationSender implements ScheduledNotificationSender {
@Override
public void send(String recipient, String message) {
System.out.println("Email sent to " + recipient);
}
@Override
public void schedule(String recipient, String message, LocalDateTime sendAt) {
System.out.println("Email scheduled for " + sendAt);
}
}SMS supports immediate sending:
@Service
public class SmsNotificationSender implements NotificationSender {
@Override
public void send(String recipient, String message) {
validateMessage(message);
System.out.println("SMS sent to " + recipient);
}
private void validateMessage(String message) {
if (message == null || message.isBlank()) {
throw new IllegalArgumentException("Message must not be empty");
}
if (message.length() > 160) {
throw new IllegalArgumentException("SMS cannot exceed 160 characters");
}
}
}Campaign scheduling requires a scheduler:
@Service
public class CampaignService {
public void scheduleCampaign(
ScheduledNotificationSender notificationSender,
Campaign campaign
) {
notificationSender.schedule(
campaign.getRecipient(),
campaign.getMessage(),
campaign.getSendAt()
);
}
}Why This Is Better
The type system now communicates capabilities clearly.
CampaignService cannot receive an implementation that does not support scheduling.
There is no:
instanceofThere is no:
UnsupportedOperationExceptionThere is no silent conversion from scheduled delivery to immediate delivery.
The design is easier to extend with future providers such as:
- Push notifications
- Slack
- Microsoft Teams
Each implementation exposes only the capabilities it genuinely supports.
27. Interview Perspective
LSP questions in experienced Java interviews are rarely limited to:
What is the Liskov Substitution Principle?
Senior-level interviews are more likely to present design scenarios.
For example:
We have several payment implementations, but one implementation cannot support refunds and throws
UnsupportedOperationException. What is wrong with this architecture?
A strong answer should discuss:
- Behavioral substitutability
- Incorrect abstraction
- Interface segregation
- Capability-based interfaces
- Composition versus inheritance
- Runtime safety
- Caller independence
- Contract testing
Another common scenario is:
An overridden method performs extra validation and rejects inputs accepted by the parent implementation. Is this safe?
The candidate should discuss whether the subtype is strengthening the parent's preconditions and thereby breaking callers relying on the parent contract.
For Spring Boot, interviews may involve multiple beans implementing the same interface.
The key question becomes:
Can every injected implementation safely satisfy the assumptions made by the service consuming that interface?
28. Interview Questions and Answers
Basic Question
Question: What is the Liskov Substitution Principle?
Answer:
LSP states that a subtype should be usable wherever its parent type is expected without breaking the correctness of the program.
In Java, this means an implementation should preserve the meaningful behavioral contract of the interface or superclass it implements.
The principle is about behavior, not merely valid inheritance syntax.
Intermediate Question
Question: Why can UnsupportedOperationException indicate an LSP violation?
Answer:
If an interface promises an operation and one implementation always throws UnsupportedOperationException, that implementation cannot satisfy the contract callers expect from the abstraction.
For example:
interface PaymentProcessor {
void process();
void refund();
}If a processor cannot refund payments, forcing it to implement refund() creates an invalid abstraction.
A better solution is usually to separate refundable capability into another interface.
Advanced Question
Question: How do preconditions and postconditions relate to LSP?
Answer:
A subtype should not unexpectedly require stronger preconditions than its parent contract.
For example, if the parent accepts every positive payment amount but a subtype accepts only amounts greater than 1000, callers using the parent contract may fail unexpectedly.
Similarly, a subtype should not weaken important guarantees made after the operation.
If the parent guarantees a non-null result, an implementation returning null would violate the caller's expectations.
Scenario-Based Question
Question: You have StorageService implementations for local disk, Amazon S3, and an archive system. The interface contains upload(), download(), delete(), and generatePublicUrl(). The archive system cannot generate URLs and throws UnsupportedOperationException. What would you change?
Answer:
I would question whether generatePublicUrl() belongs in the common StorageService.
I would model that capability separately, for example:
interface StorageService {
upload();
download();
delete();
}
interface PublicUrlStorage extends StorageService {
generatePublicUrl();
}Only implementations supporting public URLs implement the second interface.
This removes runtime capability failures and preserves substitutability.
Code-Review Question
Question: What code smells would make you investigate a possible LSP violation?
Answer:
I would look for:
UnsupportedOperationException- Empty overridden methods
instanceof- Switches based on implementation type
- Subclass-specific validation
- Unexpected
nullreturns - Different exception semantics
- Methods inherited only because of hierarchy
- Callers explicitly excluding certain implementations
These do not automatically prove a violation, but they are strong review signals.
Real-Project Question
Question: How would you enforce LSP across multiple Spring Boot implementations of an interface?
Answer:
I would first define the behavioral contract clearly.
Then I would create shared contract tests that every implementation must pass.
For external integrations, I would also test provider-specific adapters to ensure they translate external behavior into the common application contract.
Consumers should depend on the narrowest interface they need, and optional provider capabilities should be represented through separate contracts rather than unsupported methods.
29. Quick Rule to Remember
If callers must ask which implementation they received before safely using it, inspect the abstraction for an LSP violation.
30. Final Takeaway
The Liskov Substitution Principle is fundamentally about trusting abstractions.
When a Java service depends on:
PaymentProcessor
NotificationSender
StorageService
PricingStrategy
CustomerRepositoryit should not need detailed knowledge of the concrete implementation in order to use that abstraction safely.
Developers should remember:
- Inheritance does not automatically create a valid subtype relationship.
- Interfaces represent behavioral contracts.
- Unsupported inherited operations are a major warning sign.
- Subtypes should not surprise their callers.
- Optional capabilities should often be separated.
- Composition is preferable when inheritance creates an artificial relationship.
During code review, reviewers should check:
- Whether every implementation can honor the contract.
- Whether subclasses introduce stronger restrictions.
- Whether guarantees become weaker.
- Whether unsupported operations exist.
- Whether callers contain subtype-specific checks.
- Whether the abstraction represents real business capabilities.
Production code should avoid designs where:
Parent reference + valid method call = unexpected runtime failureA strong Java design makes valid behavior obvious through its types and allows implementations to be replaced without forcing callers to understand their internal differences.