Preserve expectations
A subtype should honor the promises that callers rely on from the base type.
LSP is often presented abstractly, but its practical question is simple: if code depends on a base type, can any subtype step in without surprising the caller? When the answer is no, inheritance or polymorphic design is probably lying about the relationship.
Java systems depend heavily on subtype-based design, whether through inheritance, interfaces, or framework contracts. In order-management domains, a subtype that quietly rejects previously valid behavior can destabilize pricing, payment, or shipment flows in subtle ways.
LSP matters because polymorphism only works when the abstraction stays trustworthy. If a caller uses a contract believing one set of guarantees, and a subtype silently breaks them, the design becomes brittle.
In Java, LSP issues often show up as overridden methods that throw UnsupportedOperationException, strengthen preconditions, weaken postconditions, or behave in ways the original abstraction did not prepare callers for.
The OOAD lesson is usually not “ban inheritance.” It is “model subtype relationships honestly.”
A subtype should honor the promises that callers rely on from the base type.
If the subtype cannot safely perform expected operations, the type hierarchy may be wrong.
The principle often exposes where composition or a different abstraction would be clearer.
package org.javaomnibus.ecommerce.principles;
interface ShipmentHandler {
ShipmentResult prepare(Shipment shipment);
}
final class StandardShipmentHandler implements ShipmentHandler {
@Override
public ShipmentResult prepare(Shipment shipment) {
return new ShipmentResult(true, "prepared for normal dispatch");
}
}
final class RefrigeratedShipmentHandler implements ShipmentHandler {
@Override
public ShipmentResult prepare(Shipment shipment) {
if (!shipment.requiresCooling()) {
return new ShipmentResult(false, "shipment does not require cooling");
}
return new ShipmentResult(true, "prepared with refrigerated workflow");
}
}
record Shipment(boolean requiresCooling) {}
record ShipmentResult(boolean prepared, String message) {}
Strictly preserving a base contract may expose that the abstraction is too general. That is useful information. Sometimes the right response is to split the hierarchy or move to composition rather than forcing unsafe substitution.