LSP through the classic Square-extends-Rectangle trap, and ISP through the Worker interface anti-pattern — the third and fourth SOLID letters, in code.
Published September 22, 2026
Barbara Liskov's formal statement: if S is a subtype of T, objects of type T should be replaceable with objects of type S without altering the correctness of the program. In practice: any code written against the base type must keep working, unmodified, when handed any subtype.
class Rectangle {
protected int width, height;
void setWidth(int w) { width = w; }
void setHeight(int h) { height = h; }
int area() { return width * height; }
}
class Square extends Rectangle {
@Override void setWidth(int w) { width = w; height = w; } // forces both to stay equal
@Override void setHeight(int h) { width = h; height = h; }
}
Mathematically, a square is a rectangle — so this looks reasonable. But run this code, written entirely against the Rectangle type:
void resize(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
assert r.area() == 50; // holds for Rectangle, FAILS for Square (area becomes 100)
}
Any Rectangle-typed code that assumes setting width and height independently produces width * height breaks the moment a Square is substituted in — exactly the failure LSP is defined to prevent. The bug isn't in the math (a square genuinely is a rectangle); it's that Square changes the behavioral contract Rectangle established (independent width/height mutation), which is what LSP actually protects — behavior, not just is-a relationships from a domain model.
The fix: don't model Square extends Rectangle at all — model both as implementations of a Shape interface exposing only area(), with no shared mutable setter contract to violate.
interface Worker {
void work();
void eat();
}
class HumanWorker implements Worker {
public void work() { /* ... */ }
public void eat() { /* ... */ }
}
class RobotWorker implements Worker {
public void work() { /* ... */ }
public void eat() { throw new UnsupportedOperationException(); } // forced to implement something meaningless
}
RobotWorker is forced to implement eat() even though it's meaningless for a robot — this is exactly the smell ISP names: a class is forced to depend on (and implement) methods it doesn't use. Beyond being awkward, it's dangerous — any caller holding a Worker reference can legally call .eat() on a RobotWorker and get a runtime exception instead of a compile-time guarantee that the operation makes sense.
interface Workable { void work(); }
interface Eatable { void eat(); }
class HumanWorker implements Workable, Eatable { ... }
class RobotWorker implements Workable { ... } // simply doesn't implement Eatable — no dead method
Each class implements exactly the capabilities it genuinely has. No UnsupportedOperationException landmines, and the type system itself now documents which capabilities each class actually supports.
ISP violations often cause LSP violations: a fat interface forces implementers to fake methods they don't really support (via no-ops or exceptions), and any caller programming against the interface can no longer trust that every implementation behaves the way the interface implies — which is precisely an LSP break. Splitting the interface (ISP) is frequently the fix that also restores LSP compliance.
Q: Is every is-a relationship automatically LSP-safe? A: No — Square/Rectangle is the canonical proof. Is-a from a domain-modeling standpoint ("a square is mathematically a kind of rectangle") and is-a from a behavioral substitutability standpoint (LSP's actual concern) can diverge; LSP cares about the latter.
Q: How would you detect an LSP violation in code review, concretely? A: Look for subclasses that override a method to throw an exception, return a different/narrower type than callers expect, strengthen preconditions, or weaken postconditions compared to the base type's documented contract — any of these break substitutability even if they compile cleanly.
Q: Doesn't ISP just mean 'lots of tiny interfaces everywhere'? A: The goal is interfaces segmented by cohesive capability, not interfaces minimized to one method each arbitrarily — over-splitting without a real capability boundary just adds interface-management overhead without the actual benefit ISP is after.
Q: Can a class legitimately need to implement a 'fat' interface with many methods? A: Yes, if every one of those methods is genuinely part of one cohesive capability that class actually has — ISP objects to forced, unused dependencies, not to interface size in isolation.