Every object-oriented programmer learns about inheritance in their first weeks of coding. Create a base class, extend it, override some methods—simple enough. But somewhere between those early tutorials and production systems, inheritance starts causing problems that seem to emerge from nowhere.

The Liskov Substitution Principle, formulated by Barbara Liskov in 1987, addresses a fundamental truth that many developers learn the hard way: syntactic compatibility is not the same as behavioral compatibility. Your code might compile perfectly while harboring subtle bugs that only surface when someone uses your subtype in an unexpected context.

LSP states that objects of a supertype should be replaceable with objects of a subtype without altering the correctness of the program. This sounds straightforward until you realize it's not about what your compiler accepts—it's about the promises your types make and whether your subtypes keep them.

Contract Inheritance: The Promises Your Subtypes Must Keep

When you inherit from a base class, you're not just inheriting code. You're inheriting a contract—a set of guarantees that client code depends on. This contract has three components: preconditions, postconditions, and invariants.

Preconditions define what must be true before a method executes. A subtype can weaken preconditions (accept more inputs) but never strengthen them. If your base class method accepts any positive integer, your subtype cannot suddenly require integers greater than ten. Client code that worked before would break.

Postconditions define what must be true after a method executes. Here the rule reverses: subtypes can strengthen postconditions (guarantee more) but never weaken them. If your base class promises to return a non-null result, your subtype cannot return null just because it's convenient.

Invariants are properties that must remain true throughout an object's lifetime. A rectangle's width and height can vary independently—that's part of its contract. If you create a square subtype where setting width also changes height, you've violated this invariant. Code that manipulates rectangles will produce incorrect results when handed a square, even though the inheritance hierarchy looks perfectly reasonable.

Takeaway

Inheritance isn't just about sharing code—it's about inheriting behavioral guarantees. Every override must honor the promises made by the method it replaces.

Violation Patterns: Recognizing LSP Problems in the Wild

The most obvious LSP violation is the refused bequest—a subtype that throws an exception for an inherited method. Consider a Bird base class with a fly() method. When you create a Penguin subtype that throws UnsupportedOperationException, you've created a substitutability trap. Any code iterating through birds and calling fly() will crash unexpectedly.

Empty method implementations are equally problematic, just quieter. A logging subtype whose log() method does nothing might seem harmless, but client code depending on log messages for debugging or auditing silently fails. The system compiles, the tests might even pass, but the guarantees have evaporated.

Weakened guarantees create the most insidious bugs. A caching subtype that returns stale data, a security wrapper that skips validation for performance, a mock object that doesn't maintain state correctly—these violations often work fine in normal conditions but fail catastrophically at boundaries.

Type-checking code is a diagnostic symptom. When you see instanceof checks scattered throughout your codebase, you're looking at the aftermath of LSP violations. The code is working around subtypes that don't behave like their parents, manually handling each special case. This defeats the entire purpose of polymorphism and creates maintenance nightmares as new subtypes require updates everywhere.

Takeaway

If client code needs to check which subtype it's working with, your inheritance hierarchy has failed at its primary job—making subtypes interchangeable.

Design Corrections: Rethinking Broken Hierarchies

When you discover an LSP violation, the fix is rarely a simple code change. The violation usually reveals that your abstraction was wrong from the start. The classic rectangle-square problem isn't solved by clever coding—it's solved by recognizing that a mutable square is not a behavioral subtype of a mutable rectangle.

Composition over inheritance resolves many LSP headaches. Instead of making Penguin extend Bird, give both Bird and Penguin a movement strategy. Flying birds get a FlyingMovement; penguins get a SwimmingMovement. No broken contracts, no exceptions, no special cases.

Interface segregation helps when base types promise too much. Split the bloated interface into focused ones. Not all birds fly, so don't put fly() in the Bird interface. Create Flyable for those that do. Clients that need flying things depend on Flyable; clients that just need birds depend on Bird.

Sometimes the answer is eliminating the inheritance relationship entirely. Favor shallow hierarchies with clear contracts over deep inheritance trees where behavioral compatibility becomes impossible to track. Ask whether you truly need polymorphic substitution or whether you're using inheritance as a code-sharing mechanism—a job better handled by composition or utility classes.

Takeaway

Fixing LSP violations often means redesigning your abstractions, not patching your implementations. The hierarchy that seems intuitive might not be the hierarchy that works.

The Liskov Substitution Principle isn't an academic exercise—it's a practical tool for predicting where your object hierarchies will cause pain. Violations manifest as bugs that appear when code uses your types in legitimate ways you didn't anticipate.

Designing for substitutability forces clarity. You must articulate exactly what your base types promise and verify that every subtype honors those promises. This discipline improves your designs even when you never write a subtype.

The next time you reach for inheritance, pause. Ask not whether the syntax works, but whether the behavior substitutes. Your future self—and everyone who maintains your code—will thank you.