SOLID is an acronym every developer knows but few apply consistently. The five principles exist because code that violates them becomes hard to change, test, and extend. Here's each one with a real problem and the solution.

S: Single Responsibility Principle

A class or function should have one reason to change. When a function does too much, any change to one of those things risks breaking the others.

// ❌ Does too much: changes for multiple reasons
class UserService {
  async createUser(data: CreateUserDto) {
    const user = await this.db.user.create({ data })
    await this.sendWelcomeEmail(user.email)
    await this.auditLog('user.created', user.id)
    return user
  }
}

// ✅ Each responsibility separated
class UserService {
  constructor(
    private readonly userRepo: UserRepository,
    private readonly emailService: EmailService,
    private readonly auditService: AuditService
  ) {}

  async createUser(data: CreateUserDto) {
    const user = await this.userRepo.create(data)
    await this.emailService.sendWelcome(user.email)
    await this.auditService.log('user.created', user.id)
    return user
  }
}

The correct version has three reasons to change: data repository, email template, or audit policy. The wrong version has one reason for each change to any of those three.

O: Open/Closed Principle

Software entities should be open for extension and closed for modification. Add behavior without changing existing code.

// ❌ To add payment type, modify the class
class PaymentProcessor {
  process(type: string, amount: number) {
    if (type === 'credit_card') { /* ... */ }
    else if (type === 'pix') { /* ... */ }
    // adding 'boleto' requires modifying this method
  }
}

// ✅ Adds behavior without modifying existing code
interface PaymentMethod {
  process(amount: number): Promise<PaymentResult>
}

class CreditCardPayment implements PaymentMethod {
  async process(amount: number) { /* ... */ }
}

class PixPayment implements PaymentMethod {
  async process(amount: number) { /* ... */ }
}

// To add BoletoPayment, create a new class
// No existing code is modified

L: Liskov Substitution Principle

Subtypes must be substitutable for their base types without altering the correctness of the program. If you need instanceof to decide behavior, you violated LSP.

// ❌ Subtype breaking contract
class Rectangle {
  constructor(protected width: number, protected height: number) {}
  setWidth(w: number) { this.width = w }
  setHeight(h: number) { this.height = h }
  area() { return this.width * this.height }
}

class Square extends Rectangle {
  setWidth(w: number) { this.width = w; this.height = w }
  setHeight(h: number) { this.width = h; this.height = h }
}

// Square can't substitute Rectangle without breaking expectations
function increaseWidth(rect: Rectangle) {
  rect.setWidth(rect.width + 1) // Square changes height too
}

// ✅ Composition or hierarchy preserving contract
interface Shape {
  area(): number
  scale(factor: number): Shape
}

I: Interface Segregation Principle

Clients shouldn't be forced to depend on interfaces they don't use. Large interfaces force implementations with dead code.

// ❌ Giant interface: forces incomplete implementations
interface DataStore {
  read(id: string): Promise<any>
  write(id: string, data: any): Promise<void>
  delete(id: string): Promise<void>
  subscribe(event: string, cb: Function): void
  connect(): Promise<void>
}

// ✅ Segregated interfaces
interface Reader {
  read(id: string): Promise<any>
}

interface Writer {
  write(id: string, data: any): Promise<void>
}

interface EventSource {
  subscribe(event: string, cb: Function): void
}

// Each implementation depends only on what it uses

D: Dependency Inversion Principle

High-level modules shouldn't depend on low-level modules. Both should depend on abstractions. Abstractions shouldn't depend on details. Details should depend on abstractions.

// ❌ High level depends on low level
class OrderService {
  constructor() {
    this.db = new PrismaClient()  // direct coupling
  }
}

// ✅ Both depend on abstraction
interface OrderRepository {
  save(order: Order): Promise<void>
  findById(id: string): Promise<Order | null>
}

class OrderService {
  constructor(private readonly repo: OrderRepository) {}
}

// PrismaUserRepository implements OrderRepository
// In tests, uses InMemoryOrderRepository

How to apply in daily work

SOLID isn't for applying everywhere. It's for applying where the cost of change is high: business domain, service layers, extension points. Utility functions, simple helpers, and configuration code don't need SOLID.

The guiding question: "If I need to change X, how many other things will break?" If the answer is many, refactor. If it's few, it's good enough.