Why SOLID Matters – From Fundamentals to Real Code
August 1, 2025
3 days ago
4 min read
Average reading time
Learn the SOLID principles not as rules to memorize but from first principles—understand the why behind clean, flexible, and resilient software design.
Prerequisites
- Basics of object-oriented programming (OOP)
- Familiarity with TypeScript or JavaScript
- Awareness of common software design challenges
What You'll Learn
- Understand each SOLID principle from first principles
- Learn how to apply them in real-world code
- Gain deeper reasoning skills in software architecture
Why SOLID Matters – From Fundamentals to Real Code
Software is easy to write, but hard to maintain. Features keep changing, teams grow, and bugs pop up unexpectedly. Why? Because many codebases grow without design principles.
In this post, we explore the SOLID principles—not just what they are, but why they exist, using a first principles thinking approach.
What Are First Principles?
First principles thinking means breaking down a concept to its fundamental truths, then building up your understanding from there.
In software, let’s ask:
"What makes good code?"
From first principles:
- It should be easy to change.
- It should be easy to read and reason about.
- It should be modular and testable.
- It should minimize side effects when changing.
These aren’t buzzwords. They are the foundation of good design.
SOLID is an acronym of five principles that help you write maintainable code:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Let’s understand each using first principles and real code examples.
1. Single Responsibility Principle (SRP)
A class should have only one reason to change.
First Principles View :
If one module handles multiple concerns (e.g. both file I/O and business logic), changes in one area can accidentally break another.
Analogy :
Think of a printer that also makes coffee. Fixing the printing bug might mess up the coffee timer.
1class Report {
2 generate() { /* logic */ }
3 saveToFile() { /* file I/O */ }
4}
1class Report {
2 generate() { /* logic */ }
3}
4
5class FileSaver {
6 save(report: Report) { /* file I/O */ }
7}
2. Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
First Principles View:
You shouldn't have to change existing code just to add new behavior—this reduces risk and increases stability.
1interface PaymentProcessor {
2 process(amount: number): void;
3}
4
5class PayPal implements PaymentProcessor {
6 process(amount: number) { /* PayPal logic */ }
7}
8
9class Stripe implements PaymentProcessor {
10 process(amount: number) { /* Stripe logic */ }
11}
12
13// Usage
14const processor: PaymentProcessor = new Stripe();
15processor.process(1000);
We added a new payment method without modifying old logic—just extended it.
3. Liskov Substitution Principle (LSP)
If class B is a subclass of class A, then anywhere I use A, I should be able to use B — and everything should still work.
First Principles View:
We want code that is predictable and doesn’t break when we use child classes in place of parent classes.
So we ask:
- Can I trust that any subclass won’t break expectations?
- Does the subclass behave like the base class?
Real-World Analogy:
Imagine you have a charging socket:
- It's labeled USB-A port.
- You plug in a USB fan – works fine.
- You plug in a USB mouse – works fine.
- Now you plug in a USB blender, and suddenly it explodes or blows the fuse.
Even though it physically fits, the behavior violates expectations.
That’s an LSP violation.
1class Rectangle {
2 constructor(public width: number, public height: number) {}
3
4 setWidth(w: number) {
5 this.width = w;
6 }
7
8 setHeight(h: number) {
9 this.height = h;
10 }
11
12 getArea(): number {
13 return this.width * this.height;
14 }
15}
16
17class Square extends Rectangle {
18 setWidth(w: number) {
19 this.width = w;
20 this.height = w;
21 }
22
23 setHeight(h: number) {
24 this.height = h;
25 this.width = h;
26 }
27}
28
29
30function printArea(shape: Rectangle) {
31 shape.setWidth(5);
32 shape.setHeight(10);
33 console.log(shape.getArea());
34}
35
36printArea(new Rectangle(5, 5)); // prints 50
37printArea(new Square(5, 5)); // prints 100 , WTF?
Even though Square extends Rectangle, it doesn’t behave like a normal rectangle — it breaks expectations.
1interface Shape {
2 getArea(): number;
3}
4
5class Rectangle implements Shape {
6 constructor(public width: number, public height: number) {}
7 getArea() {
8 return this.width * this.height;
9 }
10}
11
12class Square implements Shape {
13 constructor(public side: number) {}
14 getArea() {
15 return this.side * this.side;
16 }
17}
18
19This respects LSP.
Now you’re not pretending that a square is a rectangle — they both just implement Shape, which returns an area.
This respects LSP.
Golden Rule for LSP
Don’t lie with inheritance.
Ask yourself:
“Can I substitute the child for the parent without breaking the logic?”
If not, don’t inherit. Use composition or interfaces instead.