Sitemap

The Square-Rectangle Trap:

6 min readMar 9, 2026

--

Why Liskov’s Substitution Principle Matters More Than You Think

A deep dive into object-oriented design pitfalls and elegant solutions

The Tempting (But Flawed) Hierarchy

Class Hierarchy: Square inherits from Rectangle

Figure 1: The problematic inheritance chain that violates LSP

I want to tell you about the day a square broke my code.

Not dramatically; no exceptions, no stack traces. It just… quietly returned the wrong answer. The area was 64 when it should have been 96, and the program kept running like nothing happened. That kind of bug is the worst kind. And the root cause? I’d made Square inherit from Rectangle, which seemed totally reasonable at the time. Every square IS a rectangle, right?

Right. But that’s exactly the trap.

First, What Is Liskov’s Substitution Principle?

LSP is one of the five SOLID principles, introduced by Barbara Liskov back in 1987. The formal definition sounds intimidating:

“If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.”

But stripped down, it’s actually a pretty simple promise: if you swap a parent class out for a subclass, nothing should blow up or behave weirdly. Code that works with a Rectangle should work just as well when you hand it a Square; no surprises, no silent corruption.

Here’s the thing though: LSP isn’t about whether two things look similar in the real world. It’s about whether they behave the same way in your code. And that distinction is exactly where Square vs Rectangle goes wrong.

So What Actually Goes Wrong?

Let’s walk through it. Here’s a typical Rectangle class:

public class Rectangle : Shape

{

public virtual int Width { get; set; }

public virtual int Height { get; set; }

public override double Area() => Width * Height;

public override string ToString() =>

$”Rectangle: {Width} x {Height}”;

}

Now here’s where you think you’re being clever. Since a square just has equal sides, you inherit from Rectangle and override both setters to always keep Width and Height in sync:

public class Square : Rectangle

{

private int _size;

public override int Width

{

get => _size;

set => _size = value; // sets both sides!

}

public override int Height

{

get => _size;

set => _size = value; // sets both sides!

}

public int Size => _size;

}

Looks innocent. But watch what happens when someone actually uses it:

Square aSquare = new Square();

aSquare.Width = 12;

aSquare.Height = 8;

// Expected: width=12, height=8, area=96

// Actual: width=8, height=8, area=64 ✔️ silently wrong!

Setting the height to 8 silently stomps over the width you just set to 12. No error. No warning. The program happily computes 64 instead of 96 and moves on. This is the sneakiest kind of bug; the kind you spend an afternoon debugging because the stack trace gives you nothing.

Why This Actually Violates LSP

Here’s the key insight: LSP isn’t violated because the code crashed. It’s violated because Square silently changed the rules that Rectangle promised to follow. A Rectangle has a fundamental behavioral contract: Width and Height are independent. You can set one without touching the other. Square blows up that contract by secretly coupling them.

If you want to get technical about it, there are three specific violations:

• Precondition strengthening; Square requires that Width == Height at all times, but Rectangle makes no such precondition.

• Postcondition weakening; Setting Width on a Rectangle guarantees only Width changes; on a Square, it also changes Height silently.

• Invariant violation; Any method that tests width ≠ height will produce wrong results when passed a Square.

A good rule of thumb I’ve picked up: if you’re overriding a method just to break or constrain the parent’s behavior, that’s your LSP alarm going off. Listen to it.

Okay, So How Do We Fix It?

Option 1: Make Them Siblings (My Favorite)

The cleanest fix is to stop trying to make Square a special version of Rectangle altogether. Instead, give them a common abstract parent and let them each do their own thing:

Get Ntare GAMA Allan’s stories in your inbox

Join Medium for free to get updates from this writer.

public abstract class Shape

{

public abstract double Area();

public abstract override string ToString();

}

public class Rectangle : Shape

{

public int Width { get; set; }

public int Height { get; set; }

public override double Area() => Width * Height;

}

public class Square : Shape

{

public int Side { get; set; }

public override double Area() => Side * Side;

}

Now neither class has to pretend to be something it’s not. Rectangle has its own independent Width and Height. Square has its own Side. They share a contract through Shape (specifically Area() and ToString()), and that’s all they need to share.

Option 2: Make Everything Immutable

Here’s a fun realization: the whole problem only exists because Width and Height are mutable. If you construct your shapes and never change them, the substitution issue disappears:

public class Rectangle : Shape

{

public int Width { get; } // read-only

public int Height { get; } // read-only

public Rectangle(int w, int h) { Width = w; Height = h; }

public override double Area() => Width * Height;

}

public class Square : Rectangle

{

public Square(int side) : base(side, side) { }

}

With read-only properties, passing a Square anywhere a Rectangle is expected is totally safe. You can read Width and Height independently and they’ll always reflect what was set at construction time. This is actually why functional languages almost never run into this problem; immutability sidesteps the whole mess.

Option 3: Use Interfaces and Be Explicit About Capabilities

This one shifts the mindset a bit. Instead of modeling what shapes are, model what they can do:

public interface IShape

{

double Area();

string Describe();

}

public interface IResizable

{

void Resize(int width, int height);

}

// Rectangle implements both, Square only implements IShape

public class Rectangle : IShape, IResizable { … }

public class Square : IShape { … }

Now Square never claims to support resizing at all. If a method needs IResizable, a Square simply can’t be passed to it; and that’s a good thing. The type system is now expressing the actual reality of your domain instead of hiding a dangerous assumption.

The Corrected Hierarchy at a Glance

Press enter or click to view image in full size
Figure 2: The corrected hierarchy; Rectangle and Square as siblings under Shape

The Actual Lesson Here

What this example really teaches you is that math and code are different languages. In math, “a square is a special rectangle” is a fact. In code, it’s a design choice ; and whether it’s a good choice depends entirely on behavior, not geometry.

Inheritance should model behavioral compatibility, not real-world taxonomy. Before you write class Dog : Animal or class Square : Rectangle, ask: can the subclass fulfill every single promise the parent makes, completely and without side effects? If you’re hedging that answer, the inheritance is probably wrong.

The LSP test I use: “Could I swap this subclass in for the parent and have every caller be completely unaware of the change?” If the answer is “well, mostly… that’s a no.

Wrapping Up

The Square-Rectangle problem is one of my favorite examples to teach because it’s so deceptively simple. It looks fine. The code compiles. The logic seems sound. And then you get a wrong answer with zero explanation.

The three fixes; sibling classes, immutable objects, or interface segregation; all work. Pick whichever fits your situation. But they all share the same underlying idea: model what things do in your system, not just what they are in the real world.

Next time you’re about to reach for inheritance, just run the quick check: can every caller swap the subclass in for the parent without knowing or caring? If yes, go for it. If you’re second-guessing yourself, the hierarchy probably needs a rethink.

#SOLID #OOP #CSharp #SoftwareDesign #CleanCode #LiskovSubstitution #dotnet

--

--