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:
Sensor management logic changes
Database schema changes
Dashboard format changes
LLM integration changes
My Solution: Split Responsibilities
I refactored into focused classes:
Benefits I Gained
Easier testing: I could unit test
SensorFormatterwithout needing a database or LLMSimpler changes: Updating the dashboard format only touched
SensorFormatterBetter reusability: I reused
SensorRepositoryin 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
MockLLMProviderfor tests in secondsFlexible 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
NetworkScansubclass can be used interchangeablyMaintainable 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
PortCheckerToolEnvironment 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
Write it working first: I get the feature working, even if messy
Identify pain points: Where is it hard to test or extend?
Apply SOLID selectively: Refactor the painful parts
Test the refactoring: Ensure behavior didn't change
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
My SimplePortChecker MCP Server - SOLID in practice
TypeScript Handbook - Interfaces
Previous: β Software Design 101 | Next: Design Principles β
Last updated