SOLID Quick Reference

LetterPrincipleCore Idea
SSingle ResponsibilityOne class, one reason to change
OOpen/ClosedExtend without modifying existing code
LLiskov SubstitutionSubtypes must honour parent contracts
IInterface SegregationMany specific interfaces > one fat interface
DDependency InversionDepend on abstractions, not concretions

S Single Responsibility Principle

A class should have one reason to change. If multiple stakeholders or concerns drive changes to the same class, split it.

Bad: God class with multiple responsibilities

Violates SRP class UserService { // Responsibility 1: User data access saveUser(user: User): void { /* SQL INSERT */ } getUser(id: number): User { /* SQL SELECT */ } // Responsibility 2: Email sending sendWelcomeEmail(user: User): void { /* SMTP logic */ } sendPasswordResetEmail(user: User): void { /* SMTP logic */ } // Responsibility 3: PDF generation generateUserReport(user: User): Buffer { /* PDF logic */ } } // If SMTP provider changes → UserService changes // If PDF library changes → UserService changes // If database schema changes → UserService changes

Good: Each class has one responsibility

Follows SRP class UserRepository { save(user: User): void { /* SQL INSERT */ } findById(id: number): User { /* SQL SELECT */ } } class EmailService { sendWelcomeEmail(user: User): void { /* SMTP */ } sendPasswordResetEmail(user: User): void { /* SMTP */ } } class UserReportGenerator { generate(user: User): Buffer { /* PDF */ } } // Each class changes for exactly one reason

How to spot SRP violations

If you need to use "and" to describe what a class does ("it handles authentication AND sends emails AND generates reports"), it violates SRP. Each "and" is a separate responsibility.

O Open/Closed Principle

Software entities should be open for extension (add new behaviour) but closed for modification (don't change existing, tested code). New features plug in through abstractions, not if/else chains.

Bad: Modifying the class for every new payment type

Violates OCP class PaymentProcessor { process(payment: Payment): void { if (payment.type === 'credit_card') { // credit card logic } else if (payment.type === 'paypal') { // paypal logic } else if (payment.type === 'stripe') { // stripe logic — added later, modifies existing class } // Every new payment type requires modifying THIS class } }

Good: New payment types extend without modifying

Follows OCP interface PaymentMethod { process(amount: number): void; } class CreditCardPayment implements PaymentMethod { process(amount: number): void { /* credit card logic */ } } class PayPalPayment implements PaymentMethod { process(amount: number): void { /* paypal logic */ } } class StripePayment implements PaymentMethod { process(amount: number): void { /* stripe logic */ } } class PaymentProcessor { process(method: PaymentMethod, amount: number): void { method.process(amount); // never changes } } // Adding Crypto payment: create CryptoPayment class — PaymentProcessor unchanged

L Liskov Substitution Principle

Objects of a subclass must be replaceable by objects of the parent class without breaking the program. Subclasses should honour the behavioural contract of the parent — not weaken preconditions or strengthen postconditions.

Classic violation: Square extends Rectangle

Violates LSP class Rectangle { setWidth(w: number): void { this.width = w; } setHeight(h: number): void { this.height = h; } area(): number { return this.width * this.height; } } class Square extends Rectangle { setWidth(w: number): void { this.width = w; this.height = w; // square must have equal sides } setHeight(h: number): void { this.width = h; this.height = h; } } // Code that works with Rectangle: function testArea(r: Rectangle): void { r.setWidth(5); r.setHeight(3); console.log(r.area()); // expects 15 } testArea(new Rectangle()); // 15 — correct testArea(new Square()); // 9 — WRONG: Square breaks Rectangle's contract

LSP and "is-a" — Geometrically True, Behaviourally False

A square IS geometrically a rectangle. But a Square cannot substitute for a Rectangle in code that relies on independent width/height. LSP is about behavioural substitutability, not taxonomic relationships. When a subclass overrides methods to restrict behaviour, that is an LSP violation.

I Interface Segregation Principle

Clients should not be forced to depend on methods they do not use. A "fat" interface forces implementing classes to implement methods that are irrelevant to them — leading to empty stub implementations.

Violates ISP — fat interface interface Worker { work(): void; eat(): void; // humans eat; robots do not sleep(): void; // humans sleep; robots do not } class RobotWorker implements Worker { work(): void { /* robot works */ } eat(): void { /* EMPTY STUB — robots don't eat */ } sleep(): void { /* EMPTY STUB — robots don't sleep */ } } // Segregated interfaces: interface Workable { work(): void; } interface Feedable { eat(): void; } interface Restable { sleep(): void; } class HumanWorker implements Workable, Feedable, Restable { /* all three */ } class RobotWorker2 implements Workable { work(): void { /* only work */ } }

D Dependency Inversion Principle

High-level modules (business logic) should not depend on low-level modules (database, email, file system). Both should depend on abstractions (interfaces). This enables testing with mock dependencies and swapping implementations.

Violates DIP — tight coupling class UserService { private db = new MySQLDatabase(); // hard-coded dependency private email = new SendGridEmailer(); createUser(data: UserData): void { this.db.insert(data); // cannot test without a real MySQL database this.email.send(data.email, "Welcome!"); } }
Follows DIP — dependency injection interface Database { insert(data: any): void; } interface Emailer { send(to: string, msg: string): void; } class UserService { constructor( private db: Database, // depends on abstraction private email: Emailer // depends on abstraction ) {} createUser(data: UserData): void { this.db.insert(data); this.email.send(data.email, "Welcome!"); } } // Production const service = new UserService(new MySQLDatabase(), new SendGridEmailer()); // Tests — inject mocks, no real DB or email needed const service = new UserService(new MockDatabase(), new MockEmailer());

DIP and Dependency Injection Containers

Frameworks like Spring (Java), Angular (TypeScript), Laravel (PHP), and ASP.NET Core (C#) provide DI containers that automatically wire up dependencies. You define what each class needs (via constructor or annotations) and the framework injects the right implementations — making DIP effortless at scale.

How We Research and Update This Guide

We test the underlying formula or workflow, compare outputs with reliable references, and revise examples whenever the page content changes.

  • The workflow or formula is tested directly in the tool and compared against independent reference examples.
  • Examples are kept practical so readers can verify the result without hidden assumptions.
  • Pages are revised whenever the interface, calculation flow, or surrounding guidance materially changes.

Frequently Asked Questions — SOLID Principles