S — Single Responsibility Principle
A class should have only one reason to change. A UserService that handles authentication, email sending, and database queries violates SRP. Split it: AuthService, EmailService, UserRepository. Each module focuses on one concern. In React, a component that fetches data, transforms it, and renders it does too much — extract data fetching into a custom hook, transformation into a utility function, and keep the component for rendering only.
SRP doesn't mean one function per class. It means one cohesive responsibility. A DateFormatter can have format(), parse(), and validate() methods — they all relate to date formatting.
O — Open-Closed Principle
Software entities should be open for extension but closed for modification. Instead of adding more if/else branches to a payment processor when adding a new payment method, use the Strategy pattern: define a PaymentProcessor interface and implement CreditCardProcessor, PayPalProcessor, CryptoProcessor. Adding Apple Pay means creating a new class — zero changes to existing code. In TypeScript, use interfaces and dependency injection to achieve OCP naturally.
L — Liskov Substitution Principle
Subtypes must be substitutable for their base types without breaking the program. Classic violation: Square extending Rectangle — setting width on a square must also change height, breaking rectangle's contract. The fix: don't model "is-a" relationships that break behavior. In TypeScript: if function processShape(shape: Shape) works with Rectangle, it must also work correctly with any subclass. Favor composition over inheritance to avoid LSP violations entirely.
I — Interface Segregation Principle
Clients shouldn't be forced to depend on interfaces they don't use. A Worker interface with work(), eat(), sleep() forces robots to implement eat() and sleep(). Split into Workable, Eatable, Sleepable. In TypeScript, prefer small, focused interfaces: Readable, Writable, Closeable instead of one giant Stream interface. React components follow ISP naturally — props interfaces should only include what the component actually uses.
D — Dependency Inversion Principle
High-level modules shouldn't depend on low-level modules. Both should depend on abstractions. Instead of OrderService directly importing PostgresDatabase, inject a Database interface. Now you can swap PostgreSQL for MongoDB, or use a mock in tests, without changing OrderService. In React: instead of hardcoding fetch calls in components, inject data-fetching functions via props or context. DIP is the foundation of testable architecture.
Applying SOLID in Practice
Don't apply all principles upfront — that's over-engineering. Start with SRP: keep modules focused. When you need to add a feature and it requires modifying existing code, consider OCP. When testing is painful, check for DIP violations. SOLID principles are guidelines, not laws. The goal is maintainable, extensible code — sometimes breaking a principle is the right pragmatic choice.
Beyond SOLID
SOLID complements other principles: DRY (Don't Repeat Yourself), KISS (Keep It Simple), YAGNI (You Aren't Gonna Need It). Together, they form a philosophy: write code that's simple, focused, and easy to change. Learn design patterns to see how SOLID principles are applied in common solutions. Read "Clean Architecture" by Robert Martin for the full picture of how these principles create sustainable software systems.