- 5 design principles specifically aimed at Object-Oriented Programming (OOP).
- Software always changes: new features, bug fixes, refactors... It's in its nature.
- These principles guide us to adapt to those changes without ending up with unmaintainable, chaotic code.
Imagine you're building a house:
- With LEGO (SOLID code) - You place piece by piece. If something doesn't fit, you remove it and swap it without touching the rest. You can expand, reorganize, and improve on top of what you already have.
- With stone and cement (non-SOLID code) - Everything is glued together. If you make a mistake on one wall, you have to tear the whole thing down. Changing something small can mean demolishing half the building.
- A class should have one responsibility and one reason to change.
- If you need to describe your class as "it does this AND this AND this"... it's already wrong. A
Userclass shouldn't handle payments, send emails, and validate data. - Separate by actors: if the PM needs something, it goes in one class. If Marketing needs something else, it goes in another. Each actor, each responsibility.
Analogy - The restaurant:
Imagine a restaurant where the waiter does everything: greets customers, takes orders, cooks, serves food, and handles the bill. Total chaos, nothing is efficient.
That's why in a real restaurant each person has a clear role: the chef cooks, the waiter takes orders, the host greets customers. If the chef gets sick, you hire another chef without touching the waiter. That's Single Responsibility.
3 key points:
- A class should be open for extension but closed for modification. If it already works, don't touch it to add more stuff: more code, more mess, more chance of breaking it.
- Use the Strategy Pattern: create an interface, implement each strategy in its own class, and inject it via the constructor. This way you extend without modifying.
- Red flag: if you have a
switchorif/elsethat grows every time you add something new, you need to apply OCP.
Analogy - Pizza and toppings:
The pizza base is already made and works. Want pepperoni? Put it on top. Mushrooms? On top. You never tear apart the dough to stuff ingredients inside. The base is closed (you don't touch it), but you can extend it with as many toppings as you want.
Without OCP: you destroy the entire dough every time the customer asks for a new ingredient.
3 key points:
- A child class that inherits from a parent must be able to replace it without causing failures. If it inherits, it's supposed to maintain the same properties and behaviors.
- When inheritance doesn't fit, use composition. Ask yourself: "Does this have an X?" instead of "Is this an X?". Classes are built from properties and behaviors, not just hierarchies.
- Red flag: if a child class returns
nullor throws anErrorbecause it can't fulfill the parent's behavior, the inheritance design is wrong.
Analogy - The restaurant:
You have a base class
Employeewith a functionserveCustomer(). The Waiter serves customers, the Host does too. Both can replaceEmployeewithout issues.But the Chef doesn't serve customers. If you force him to inherit
serveCustomer(), he'll end up with athrow Error("I cook, I don't serve"). That breaks Liskov. The solution: don't force that inheritance, create a different structure for Chef.
3 key points:
- Don't force classes to implement methods they don't use. Focus them on what they can do, not what they are.
- Small, focused interfaces are always better than one giant interface with too many responsibilities.
- Red flag: if a method returns
0,null,undefined, or throws an error because it doesn't apply, the interface is doing too much.
Analogy - The remote control:
You buy a universal remote with 100 buttons. You only use 5 for your TV. The rest get in the way, confuse you, and serve no purpose. A remote with just the 5 buttons you need would be much better: power on, power off, volume up, volume down, and change channel. Small, focused, and no junk.
- LSP - You implement something you can't do. It's dangerous: it can break in production with
throw Erroror unexpected behavior. - ISP - You implement something you don't need. It doesn't break, but it's noise: methods returning
nullor0for no reason.
In one sentence:
- LSP - Split so nobody can lie (promise something they can't deliver).
- ISP - Split so nobody implements junk (carry useless baggage).
Fix for both: smaller, more focused interfaces and classes.
3 key points:
- A class's business logic cannot depend on concrete details like MySQL, Redis, or Postgres directly inside it.
- Injection: pass dependencies through the constructor. The class doesn't create anything internally, it receives everything from outside. This way, if you switch from Postgres to MySQL or want to test with a
FakeDB, you don't touch the class. - Inversion: in the constructor, don't use concrete classes, use interfaces. Instead of receiving
MongoDBorRedis, receive aDBInterfacethat any of them can implement.
Analogy - The online store:
Your
CheckoutServiceonly knows it has aPaymentProcessor. It doesn't care how payment works internally. Want to switch from Stripe to PayPal? Swap the implementation, the checkout doesn't notice. Want to test? Inject aFakeProcessor. Everything works because the logic depends on the abstraction, not the detail.
solid/
├── README.md ← You are here
│
├── docs/
│ ├── solid-simple.md Quick SOLID overview (concise)
│ ├── solid-detailed.md Full practical guide with examples
│ └── mistakes.md Common mistakes by round
│
├── images/
│ ├── diagrams/ Technical diagrams (class/interface relationships)
│ └── analogies/ Visual metaphors for each principle
│
├── phase-1-refactor-dojo/
│ ├── README.md Full guide: 6 rounds + solutions explained
│ ├── round-1-correction.md Detailed correction for round 1
│ ├── exercises/ Bad code - the problem to refactor
│ │ ├── exercise-1.ts Round 1: SRP - The God Method
│ │ ├── exercise-2.ts Round 2: OCP - The Growing Switch
│ │ ├── exercise-3.ts Round 3: LSP - The Sneaky Override
│ │ ├── exercise-4.ts Round 4: ISP - The Swiss Army Interface
│ │ ├── exercise-5.ts Round 5: DIP - The Concrete Trap
│ │ └── exercise-6.ts Round 6: MIX - The Real-World Mess
│ └── solutions/ Refactored code - the correct solution
│ ├── exercise-1.ts
│ ├── exercise-2.ts
│ ├── exercise-3.ts
│ ├── exercise-4.ts
│ ├── exercise-5.ts
│ └── exercise-6.ts
│
├── phase-2-code-review/
│ ├── README.md Full guide: 3 PRs + solutions explained
│ ├── exercises/ Bad code - the PR to review
│ │ ├── exercise-1.ts PR #1: Notification Manager
│ │ ├── exercise-2.ts PR #2: Discount Engine
│ │ └── exercise-3.ts PR #3: User Roles & Permissions
│ └── solutions/ Refactored code - the correct solution
│ ├── exercise-1.ts
│ ├── exercise-2.ts
│ └── exercise-3.ts
│
├── package.json
└── tsconfig.json
# Run any exercise
npx tsx phase-1-refactor-dojo/exercises/exercise-1.ts
# Compare with the solution
npx tsx phase-1-refactor-dojo/solutions/exercise-1.ts
# Same for phase 2
npx tsx phase-2-code-review/exercises/exercise-1.ts
npx tsx phase-2-code-review/solutions/exercise-1.ts











