Java Omnibus
OOAD & Architecture / Design principles / Liskov Substitution Principle in Java
Design Principles Java Order Management
Design Principles

Liskov Substitution protects polymorphism by asking whether a subtype truly preserves the expectations of its 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.

Why This Topic Matters

Why it matters in real Java systems

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.

Core Explanation

The design idea behind this topic

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

Concept Breakdown

Key ideas to hold onto

Concept

Preserve expectations

A subtype should honor the promises that callers rely on from the base type.

Concept

Do not narrow behavior unexpectedly

If the subtype cannot safely perform expected operations, the type hierarchy may be wrong.

Concept

LSP supports honest modeling

The principle often exposes where composition or a different abstraction would be clearer.

Java Example

Java example in the Order Management domain

Java example: honest contracts for shipment handlers
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) {}
Code Walkthrough

Step by step

  1. Both handlers honor the same contract: they attempt preparation and return a ShipmentResult instead of surprising the caller with incompatible behavior.
  2. The variation is communicated through results, not through a contract violation.
  3. If a subtype needed an incompatible API or stronger assumptions, the abstraction itself should be reconsidered.
  4. LSP keeps the polymorphic surface predictable.
Real-World Usage

Where this shows up in practice

  • Payment providers, notification channels, and shipment handlers that share one interface
  • Library or framework contracts where callers rely on common behavior guarantees
  • Reviewing inheritance hierarchies that have accumulated special-case overrides
Trade-Offs

When not to over-apply it

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.

Common Mistakes

Frequent mistakes to watch for

  • Using inheritance for reuse instead of for a true subtype relationship
  • Throwing UnsupportedOperationException from methods that callers reasonably expect to work
  • Strengthening input requirements in a subtype
  • Ignoring LSP because the code compiles and the tests are narrow
Related Pages

Related concepts


What To Read Next