Part 5: SOLID Principles and Design Patterns - Production-Ready Code

Introduction

My first large codebase became unmaintainable at 50,000 lines. Changing one feature broke three others. Adding tests required mocking 15 dependencies. New developers took weeks to understand the architecture. The problem? I violated every SOLID principle and mixed design patterns incorrectly.

I refactored following SOLID principles. Single Responsibility: each class did one thing. Open/Closed: extended behavior without modifying code. Liskov Substitution: subclasses worked anywhere parent worked. Interface Segregation: clients only depended on methods they used. Dependency Inversion: depended on abstractions, not concretions. Combined with proven design patterns, the system became maintainable, testable, and extensible.

This article teaches you the five SOLID principles, essential design patterns, and how to build production-ready TypeScript applications.

SOLID Principles

S - Single Responsibility Principle (SRP)

A class should have one, and only one, reason to change.

Problem: Multiple Responsibilities

// Bad - User class does too much
class User {
    constructor(
        public id: string,
        public username: string,
        public email: string,
        public password: string
    ) {}
    
    // Responsibility 1: User data management
    updateEmail(email: string): void {
        this.email = email;
    }
    
    // Responsibility 2: Password hashing
    hashPassword(): void {
        this.password = crypto.createHash('sha256').update(this.password).digest('hex');
    }
    
    // Responsibility 3: Database operations
    save(): void {
        console.log(`Saving user ${this.id} to database`);
        // db.query('INSERT INTO users...');
    }
    
    // Responsibility 4: Email notifications
    sendWelcomeEmail(): void {
        console.log(`Sending welcome email to ${this.email}`);
        // emailService.send(...);
    }
    
    // Responsibility 5: Validation
    validate(): boolean {
        return this.username.length >= 3 && this.email.includes('@');
    }
}

Problems:

  • User class changes when password hashing algorithm changes

  • User class changes when database schema changes

  • User class changes when email template changes

  • Hard to test each concern in isolation

  • Cannot reuse password hashing in other classes

Solution: Separate Responsibilities

Benefits:

  • Each class has one reason to change

  • Easy to test each class in isolation

  • Can reuse PasswordService anywhere

  • Can swap database or email provider easily

O - Open/Closed Principle (OCP)

Software entities should be open for extension, but closed for modification.

Problem: Modifying Existing Code

Problems:

  • Must modify PaymentProcessor for each new payment type

  • Risk breaking existing payment types

  • All logic in one place - hard to maintain

  • Cannot test payment types independently

Solution: Extend Through Abstraction

L - Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types.

Problem: Broken Substitution

Problem: Square changes behavior of setWidth/setHeight, breaking expectations.

Solution: Proper Abstraction

I - Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use.

Problem: Fat Interface

Solution: Segregated Interfaces

D - Dependency Inversion Principle (DIP)

Depend on abstractions, not concretions.

Problem: High-level Depends on Low-level

Problems:

  • Cannot swap MySQL for PostgreSQL

  • Cannot test OrderService without MySQL

  • OrderService knows about low-level database details

Solution: Depend on Abstraction

Essential Design Patterns

Factory Pattern

Create objects without specifying exact class:

Singleton Pattern

Ensure only one instance exists:

Observer Pattern

Subscribe to events:

Repository Pattern

Separate data access logic:

Decorator Pattern

Add behavior dynamically:

Best Practices

1. Apply SOLID from the Start

Start with good design, don't wait for refactoring.

2. Choose Right Pattern for Problem

Don't force patterns where they don't fit.

3. Keep It Simple

Don't over-engineer. Add complexity only when needed.

4. Test-Driven Design

If it's hard to test, the design needs improvement.

5. Refactor Continuously

Improve design as requirements evolve.

Conclusion

You've completed the OOP 101 series! You now understand:

Part 1: Classes, objects, constructors, methods, static members

Part 2: Encapsulation, access modifiers, getters/setters, information hiding

Part 3: Inheritance, polymorphism, abstract classes, method overriding

Part 4: Interfaces, composition, dependency injection, strategy pattern

Part 5: SOLID principles, essential design patterns, production-ready code

Next Steps

  1. Practice SOLID: Refactor existing code using SOLID principles

  2. Study More Patterns: Learn Command, Adapter, Proxy patterns

  3. Build Projects: Apply OOP in real applications

  4. Read Clean Code: Study "Clean Code" by Robert Martin

  5. Domain-Driven Design: Explore DDD for complex domains

Resources

  • Books:

    • "Design Patterns" by Gang of Four

    • "Clean Code" by Robert C. Martin

    • "Head First Design Patterns"

  • Practice:

    • Refactoring.guru for design patterns

    • Code katas with OOP focus

    • Open source TypeScript projects

Keep coding, keep learning, and build maintainable systems!


Based on years of building production TypeScript applications, from monoliths to microservices, learning SOLID principles and design patterns through real-world experience.

Last updated