The four pillars
- Encapsulation — hide internal state; expose behavior. Invariants are protected because state only changes through methods.
- Abstraction — model the essentials, hide the mechanism. Callers depend on what, not how.
- Inheritance — share/extend behavior via an "is-a" relationship. Powerful but over-used; prefer composition ("has-a") for flexibility.
- Polymorphism — one interface, many implementations; the caller doesn't branch on type.
SOLID
| Principle | The test | |
|---|---|---|
| S | Single Responsibility | "One reason to change." A class does one job. |
| O | Open/Closed | Open to extension, closed to modification — add a class, don't edit a switch. |
| L | Liskov Substitution | A subtype must be usable anywhere its base is, without surprises. |
| I | Interface Segregation | Many small interfaces beat one fat one; don't force unused methods. |
| D | Dependency Inversion | Depend on abstractions, not concretions. |
Dependency Inversion in one example
The whole principle is one arrow flip. Before: high-level policy points down at a concrete detail. After: both point at an abstraction — the detail now depends on the interface, not the other way around.
// ❌ High-level policy welded to a concrete detail
class OrderService {
private email = new SmtpEmailSender(); // can't swap, can't test
confirm(o: Order) { this.email.send(o.userEmail, "Confirmed"); }
}
// ✅ Depend on an abstraction; inject the detail
interface Notifier { send(to: string, msg: string): void; }
class OrderService {
constructor(private notifier: Notifier) {} // inverted
confirm(o: Order) { this.notifier.send(o.userEmail, "Confirmed"); }
}
// Now SMTP, SMS, or a fake for tests all satisfy Notifier — Open/Closed too.
The goal is code that's easy to change and test. Quote the principle, then show
the smell it removes (a giant switch, an untestable new, a subtype that
throws on a base method). Interviewers want the judgment, not the acronym.
Composition over inheritance
Deep inheritance trees are rigid (a change high up ripples down) and force a single axis of variation. Composition lets you assemble behavior from small parts and swap them at runtime — it's why Strategy, Decorator and Dependency Injection all favor "has-a".
The left side pays 2^N classes for N independent choices; the right side pays one interface per axis of variation. "What varies here?" is the question that leads you to every pattern on the design patterns page.
Practice — level up
SOLID becomes muscle memory by modeling small things cleanly: one responsibility per class, behavior composed rather than inherited, an invariant held behind a tidy interface. These "design a structure" drills train exactly that.
Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.
Warm-up — one class, one invariant
- Min StackEasy
Hold an extra invariant (the running min) behind push/pop — encapsulation, Single Responsibility.
Core — an interface that hides its guts
Callers see operations, never the internals.- Design HashMapEasy
A clean put/get/remove contract over hidden buckets — abstraction and information hiding.
- LRU CacheMedium
Compose a map + linked list behind get/put — composition over inheritance, done right.
Stretch — pick internals the API implies
- Design Browser HistoryMedium
Choose internals that keep every operation cheap — Open/Closed against future operations.