SOLID PrinciplesLiskov Substitution PrincipleHard⏱️ ~4 min

LSP Trade-offs: When to Use vs Alternatives

LSP is essential for correct inheritance, but not every relationship requires inheritance. Understanding when LSP compliance is difficult helps you choose better design alternatives.

When LSP Inheritance is Appropriate

Use inheritance when:
  • Subtype genuinely "is-a" specialized version of the supertype behaviorally (not just structurally)
  • All operations of the parent make sense for the child
  • The child can fulfill all contracts without weakening them
  • Polymorphic substitution provides real value to client code

Example: Vehicle hierarchy where Car, Motorcycle, and Truck all support start(), stop(), accelerate(), and brake() operations with consistent semantics.

When to Choose Alternatives

Composition Pattern
Use when: Object needs capabilities from multiple sources or behaviors change dynamically.

Example: Parking spot with different pricing strategies. Use PricingStrategy composition instead of PremiumSpot extends ParkingSpot.
Interface Segregation
Use when: Different objects need different subsets of operations.

Example: Payment methods where some support installments, others do not. Use InstallmentCapable interface instead of forcing all payment types to implement it.

Common LSP Violation Patterns

Pattern 1: Refused Bequest

Subclass inherits methods it cannot or should not implement.

class ReadOnlyCollection extends Collection {
add(item) { throw new UnsupportedOperationException() }
}
⚠ Solution:

Create separate ReadableCollection and MutableCollection interfaces. Collection extends only ReadableCollection. This way, readonly collections do not inherit mutation methods they cannot support.

Pattern 2: Strengthened Preconditions

Subclass adds restrictions that parent did not have.

class Account {
withdraw(amount) { /* accepts any positive amount */ }
}

class SavingsAccount extends Account {
withdraw(amount) {
if (amount > balance - minimumBalance) throw Error
}
}
⚠ Solution:

Move minimum balance logic to Account as an optional constraint configured per account type, or create a separate RestrictedAccount interface that clients explicitly opt into when they need withdrawal limits.

Pattern 3: Weakened Postconditions

Subclass returns less information or weaker guarantees than parent promised.

class UserRepository {
findById(id): User { /* guarantees User or throws */ }
}

class CachedUserRepository extends UserRepository {
findById(id): User { /* may return null on cache miss */ }
}
⚠ Solution:

Ensure CachedUserRepository falls back to the actual data source when cache misses, maintaining the non-null guarantee. If null returns are legitimate, change the parent signature to findById(id): User | null so all implementations align.

When LSP Compliance is Overkill

  • Single concrete implementation: If you have only one subclass and no plans for polymorphism, inheritance overhead may not be justified. Use composition or direct implementation.
  • DTO (Data Transfer Object) hierarchies: Simple data containers without behavior do not need strict LSP. Structural compatibility suffices, but be cautious if you later add methods.
  • Framework extension points: If framework contract is well-defined and stable (like HttpServlet), subclasses naturally comply. The hard work is in framework design, not application code.
Interview Tip: In machine coding rounds, when you encounter a tricky hierarchy (like different vehicle types with different capabilities), explicitly discuss LSP trade-offs. Explain why you chose composition over inheritance or vice versa. Show you understand the principle, not just the pattern.

Decision Framework

Ask:
1. Can the subclass honor ALL parent contracts without exceptions?
NO → Use composition or separate interfaces

2. Does polymorphic substitution provide real value?
NO → Maybe inheritance is not needed at all

3. Are there multiple unrelated capabilities being mixed?
YES → Use multiple interfaces instead of deep hierarchy

4. Does the relationship feel forced or require workarounds?
YES → Reconsider the abstraction boundary
💡 Key Takeaways
LSP is essential when polymorphic substitution is required
Refused bequest pattern indicates wrong inheritance hierarchy
Composition often better than inheritance when behaviors are optional or dynamic
Strengthened preconditions and weakened postconditions violate LSP
Evaluate whether polymorphism provides value before choosing inheritance
📌 Examples
1ReadOnlyCollection throwing UnsupportedOperationException violates LSP
2SavingsAccount adding withdrawal restrictions strengthens preconditions
3Parking spot with pricing strategy uses composition instead of inheritance
← Back to Liskov Substitution Principle Overview
LSP Trade-offs: When to Use vs Alternatives | Liskov Substitution Principle - System Overflow