Liskov Substitution Principle

19 min read

Object-Oriented Design and SOLID Review — review Java subtypes and interface implementations for behavior that breaks caller assumptions.

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:

JAVA
public interface PaymentProcessor {
    PaymentResult process(PaymentRequest request);
}

Different implementations may exist:

JAVA
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:

JAVA
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 UnsupportedOperationException for 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:

JAVA
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:

JAVA
public interface NotificationSender {
    void send(Notification notification);
}

The application assumes that calling send() sends the supplied notification or reports a meaningful failure.

Now imagine:

JAVA
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 null where 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:

JAVA
public abstract class PaymentProcessor {
    public abstract PaymentResult process(PaymentRequest request);
    public abstract void refund(String transactionId);
}

The credit-card implementation supports both operations:

JAVA
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:

JAVA
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:

JAVA
@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:

JAVA
PaymentProcessor processor = new BankTransferPaymentProcessor();
processor.refund("TXN-1001");

But it fails at runtime.

Caller Knowledge Leakage

Developers may eventually introduce:

JAVA
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:

BankTransferPaymentProcessor cannot satisfy the refund() contract and therefore is not safely substitutable for PaymentProcessor. Consider separating refund capability from payment processing instead of throwing UnsupportedOperationException.

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.

JAVA
public interface PaymentProcessor {
    PaymentResult process(PaymentRequest request);
}

Create a dedicated contract for refundable payment mechanisms.

JAVA
public interface RefundablePaymentProcessor extends PaymentProcessor {
    RefundResult refund(String transactionId);
}

Card payments support both:

JAVA
@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:

JAVA
@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:

JAVA
@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:

JAVA
PaymentProcessor

because all implementations genuinely support process().

Refund Is an Explicit Capability

Only implementations supporting refunds implement:

JAVA
RefundablePaymentProcessor

Unsupported Operations Disappear

There is no reason to write:

JAVA
throw new UnsupportedOperationException(...);

Invalid Usage Becomes Harder

RefundService requires:

JAVA
RefundablePaymentProcessor

Therefore, developers cannot accidentally pass a normal non-refundable PaymentProcessor.

Caller Logic Becomes Simpler

There is no need for:

JAVA
instanceof
provider-type checks
exception-based capability detection

The Java type system now represents the business rules.

12. Bad Code vs Improved Code

AreaBad DesignImproved Design
AbstractionAssumes every payment supports refundModels refund as separate capability
Runtime safetyUnsupported operations fail at runtimeUnsupported usage is prevented structurally
ReadabilityBusiness capability is unclearCapability is obvious from interface
MaintainabilityNew processors may require conditionsNew processors implement only applicable contracts
TestabilityTests must handle unsupported behaviorContracts can be tested independently
ReliabilityValid-looking calls may failValid 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:

JAVA
interface PaymentGateway {
    pay();
    refund();
    cancel();
    capture();
}

But capabilities differ.

A credit-card authorization may support:

JAVA
pay
refund
cancel
capture

A bank transfer may support:

JAVA
pay

A wallet may support:

JAVA
pay
refund

Cash 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:

JAVA
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:

JAVA
UnsupportedOperationException
IllegalStateException
NullPointerException

Incorrect 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:

JAVA
Card -> success
Wallet -> success
Bank transfer -> failure

Growing 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:

JAVA
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:

JAVA
@Override
public Receipt generateReceipt(Order order) {
    return null;
}

If callers expect a receipt, substitutability is broken.

Adding Subtype Checks

Example:

JAVA
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:

JAVA
Implementation A -> IllegalArgumentException
Implementation B -> returns null
Implementation C -> NullPointerException

unless such differences are intentionally defined.

Empty Input

An interface accepting collections should clearly define behavior for empty collections.

Boundary Values

If the parent supports:

JAVA
amount > 0

a 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:

JAVA
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:

JAVA
public interface DocumentService {
    Document getDocument(String documentId, UserContext user);
}

Suppose the normal implementation verifies authorization.

A new implementation:

JAVA
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:

JAVA
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:

JAVA
UnsupportedOperationException
return null
empty implementation
no-op methods
instanceof

These often reveal incorrect abstractions.

Step 3: Separate Capabilities

Create narrower interfaces where necessary.

For example:

JAVA
PaymentProcessor
RefundablePaymentProcessor
CancellablePaymentProcessor

Step 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:

JAVA
@Override
public void refund() {
    throw new UnsupportedOperationException();
}

The abstraction probably contains a capability that the subtype does not possess.

Empty Overrides

Avoid:

JAVA
@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:

JAVA
if (service instanceof A) {
    ...
} else if (service instanceof B) {
    ...
}

This defeats the purpose of polymorphism.

Returning Fake Success

Avoid:

JAVA
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 UnsupportedOperationException for a parent method?
  • Does any overridden method silently do nothing?
  • Does any subtype return null where 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 instanceof checks?
  • 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

  1. *This implementation throws UnsupportedOperationException for an operation required by the parent contract. Can we separate this capability into a dedicated interface?*
  1. *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?*
  1. *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.*
  1. *Returning null here changes the expected behavior of this interface and forces implementation-specific null handling on callers. Can we preserve the existing contract?*
  1. *This override is intentionally empty. If this implementation does not support cancellation, cancellation probably should not be part of its interface.*
  1. *Can we split PaymentProcessor and RefundablePaymentProcessor so unsupported refund operations cannot be called?*
  1. *This implementation bypasses the authorization validation performed by the other implementations. The security guarantees of the abstraction should remain consistent.*
  1. *We now have instanceof checks for three implementations. That is likely to become harder to maintain as additional providers are introduced.*
  1. *Please add contract tests that run against all implementations of this interface.*
  1. *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.

JAVA
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:

JAVA
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:

JAVA
notificationService instanceof SmsNotificationService

This 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.

JAVA
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:

JAVA
@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:

JAVA
@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:

JAVA
@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:

JAVA
instanceof

There is no:

JAVA
UnsupportedOperationException

There is no silent conversion from scheduled delivery to immediate delivery.

The design is easier to extend with future providers such as:

  • Push notifications
  • WhatsApp
  • 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:

JAVA
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:

JAVA
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 null returns
  • 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:

JAVA
PaymentProcessor
NotificationSender
StorageService
PricingStrategy
CustomerRepository

it 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:

JAVA
Parent reference + valid method call = unexpected runtime failure

A strong Java design makes valid behavior obvious through its types and allows implementations to be replaced without forcing callers to understand their internal differences.