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
- 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
Example: Parking spot with different pricing strategies. Use
PricingStrategy composition instead of PremiumSpot extends ParkingSpot.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
Subclass inherits methods it cannot or should not implement.
add(item) { throw new UnsupportedOperationException() }
}
Create separate ReadableCollection and MutableCollection interfaces. Collection extends only ReadableCollection. This way, readonly collections do not inherit mutation methods they cannot support.
Subclass adds restrictions that parent did not have.
withdraw(amount) { /* accepts any positive amount */ }
}
class SavingsAccount extends Account {
withdraw(amount) {
if (amount > balance - minimumBalance) throw Error
}
}
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.
Subclass returns less information or weaker guarantees than parent promised.
findById(id): User { /* guarantees User or throws */ }
}
class CachedUserRepository extends UserRepository {
findById(id): User { /* may return null on cache miss */ }
}
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.
Decision Framework
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