SOLID Principles

Author: Htunn Thu Thu

Date: January 4, 2026

Tags: #SoftwareDesign #TypeScript #SOLID #BestPractices

Introduction

SOLID principles transformed how I write code. When I started building my MCP server for network security, I crammed everything into a single file. Authentication, port scanning, SSL validationβ€”all tangled together. Adding a new feature meant touching dozens of lines, and bugs appeared in unexpected places.

After refactoring using SOLID principles, my codebase became modular, testable, and easy to extend. This guide shares what I learned applying these principles to real TypeScript projects.

What is SOLID?

SOLID is an acronym for five design principles that make software more maintainable:

  • Single Responsibility Principle

  • Open/Closed Principle

  • Liskov Substitution Principle

  • Interface Segregation Principle

  • Dependency Inversion Principle

Let me show you how I applied each principle in my projects.


Single Responsibility Principle (SRP)

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

The Problem I Had

In my IoT monitoring platform, I initially created a monolithic SensorManager class:

This class had four reasons to change:

  1. Sensor management logic changes

  2. Database schema changes

  3. Dashboard format changes

  4. LLM integration changes

My Solution: Split Responsibilities

I refactored into focused classes:

Benefits I Gained

  • Easier testing: I could unit test SensorFormatter without needing a database or LLM

  • Simpler changes: Updating the dashboard format only touched SensorFormatter

  • Better reusability: I reused SensorRepository in other parts of my IoT platform


Open/Closed Principle (OCP)

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

The Problem I Had

In my Agentic LLM Search project, I needed to support multiple LLM providers:

Every new provider meant modifying LLMService, increasing the risk of breaking existing functionality.

My Solution: Plugin Architecture

I created an abstraction that allows extension without modification:

Benefits I Gained

  • Zero risk to existing code: Adding Anthropic support didn't touch Azure or local providers

  • Easy testing: I created a MockLLMProvider for tests in seconds

  • Flexible deployment: Different environments use different providers without code changes


Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.

The Problem I Had

In my MCP server, I created a hierarchy for different scan types:

My Solution: Proper Abstraction

I redesigned the hierarchy to ensure substitutability:

Benefits I Gained

  • Predictable behavior: Any NetworkScan subclass can be used interchangeably

  • Maintainable hierarchy: Adding new scan types follows clear rules

  • Better testing: Mock scans work exactly like real ones


Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they don't use.

The Problem I Had

In my IoT platform, I created a fat interface for all sensor operations:

My Solution: Focused Interfaces

I split the fat interface into smaller, focused ones:

Benefits I Gained

  • Simpler implementations: Basic sensors don't carry unnecessary baggage

  • Flexible composition: Services depend only on interfaces they actually use

  • Easier testing: Mock only the interfaces you need for each test


Dependency Inversion Principle (DIP)

Depend on abstractions, not concretions. High-level modules should not depend on low-level modules.

The Problem I Had

My MCP server initially had tight coupling:

My Solution: Dependency Injection

I inverted the dependency by depending on abstractions:

Benefits I Gained

  • Testability: I can inject mock scanners for unit tests

  • Flexibility: Switch between Nmap and native scanning without changing PortCheckerTool

  • Environment adaptation: Use different implementations in Docker vs local development


Real-World Application: Refactoring My MCP Server

Here's how I applied all SOLID principles together when refactoring my SimplePortChecker MCP:


Common Mistakes I Made

Mistake 1: Over-Engineering

Early on, I applied SOLID too zealously and created unnecessary abstractions. Not every class needs an interfaceβ€”only create them when you genuinely need flexibility or testability.

Rule of thumb I follow: Create abstraction when you have (or foresee) multiple implementations, not "just in case."

Mistake 2: Forgetting the "Why"

I sometimes created single-method interfaces thinking I was following ISP, but they added complexity without value. Interfaces should represent meaningful contracts, not just split methods arbitrarily.

Mistake 3: Incomplete LSP

I had subclasses that threw "Not Supported" exceptions for inherited methods. This violated LSP. Better to rethink the hierarchy or use composition instead.


Practical Tips

When to Apply SOLID

βœ… Apply when:

  • Building reusable libraries or frameworks

  • Code will be maintained by multiple people

  • Requirements are likely to change

  • You need comprehensive testing

⚠️ Be pragmatic when:

  • Writing scripts or prototypes

  • Deadlines are tight (but plan to refactor)

  • The code is genuinely simple and unlikely to change

My Workflow

  1. Write it working first: I get the feature working, even if messy

  2. Identify pain points: Where is it hard to test or extend?

  3. Apply SOLID selectively: Refactor the painful parts

  4. Test the refactoring: Ensure behavior didn't change

  5. Document decisions: Note why I chose certain patterns


Conclusion

SOLID principles aren't academic rulesβ€”they're practical guidelines I use daily. They've made my TypeScript projects:

  • Easier to test: Dependency injection enables comprehensive unit testing

  • Faster to modify: New features don't require rewriting existing code

  • More reliable: Changes in one area don't break unrelated functionality

  • Better documented: Code structure itself communicates intent

The key is applying them pragmatically. Start with SRP and DIPβ€”they provide the most value. Add the others as complexity grows.

What's Next?

Now that you understand SOLID, explore how these principles apply to design patterns:

References


Previous: ← Software Design 101 | Next: Design Principles β†’

Last updated