BackDev Patterns & Practices
Solid Principles//

Solid Principles

A practical series on the SOLID principles with examples and patterns to help you write maintainable, testable, and scalable TypeScript applications (Angular & Next.js).

This guide unpacks the SOLID principles one by one so that each concept stops feeling like abstraction and becomes a concrete technique you can apply today. You will see mental models, anti-patterns, and an evolving story that starts with a single responsibility and ends with a dependency graph that prefers interfaces over implementations.

SOLID is an acronym coined by Robert C. Martin (Uncle Bob) that captures five design principles for writing maintainable, scalable, and testable code. Whether you are building a C# backend or a TypeScript frontend, these ideas map onto the same core truths: keep modules small, respect contracts, and depend on abstractions, not concrete details.

1. Single Responsibility Principle (SRP)

Core Idea: A class or module should have one and only one reason to change. If you find yourself listing more than one responsibility, you have already violated it.

Why It Matters: When a class handles multiple concerns, say, both parsing user input and sending emails, then a change to the email logic forces you to re-test the parsing. You become trapped in a web of side effects. Splitting responsibilities isolates risk and makes testing simpler.

In Practice: Instead of a UserService that does everything, create a UserValidator, a UserRepository, and an EmailNotifier. Each class has a single reason to change: validator changes if rules change, repository if the database schema changes, notifier if the email template changes.

Warning Signs: Class names like “Manager,” “Service,” or “Handler” that hint at multiple jobs; test files that cover wildly different behaviors in one setup; methods with names like “ProcessEverything” or “DoAll.”

2. Open/Closed Principle (OCP)

Core Idea: A class should be open for extension but closed for modification. New behavior should arrive through inheritance, composition, or interfaces, not by rewriting existing code.

Why It Matters: Every time you crack open a working class to add a feature, you risk introducing bugs and invalidating tests. By designing for extension, new features land in new code, leaving proven logic untouched.

In Practice: Instead of modifying a Logger class to support multiple output formats, create an abstract Logger base class and derive FileLogger, ConsoleLogger, and CloudLogger from it. In TypeScript, use interfaces and strategy patterns to swap implementations.

Warning Signs: Large if-else chains that check types or string enums; comments saying “TODO: refactor this for the next feature”; modifications to old code every sprint.

3. Liskov Substitution Principle (LSP)

Core Idea: Objects of a derived class should be able to replace objects of a base class without breaking the application.

Why It Matters: Liskov ensures that inheritance hierarchies remain predictable and safe. If your code treats objects as instances of their base type, swapping a derived type should never cause surprises.

In Practice: A Rectangle class has SetWidth and SetHeight methods. A Square might try to keep its sides equal, violating the contract. Liskov says: if Square inherits from Rectangle, it must fulfill all Rectangle promises without surprise behavior.

Warning Signs: Derived classes that throw “NotImplementedException”; subclasses that silently ignore method calls; conditional checks like if (obj instanceof SpecificType) scattered through your logic.

4. Interface Segregation Principle (ISP)

Core Idea: Clients should never be forced to depend on interfaces they do not use.

Why It Matters: Fat interfaces create coupling. If a small class depends on a huge interface, it becomes coupled to things it does not care about. Segregating interfaces shrinks the surface area.

In Practice: Instead of one massive IDataProvider interface, split it into IReader, IWriter, and ICache. Classes that only read data depend only on IReader.

Warning Signs: Interfaces with dozens of methods; classes implementing interfaces but leaving most methods empty; tests that mock huge objects just to test a small feature.

5. Dependency Inversion Principle (DIP)

Core Idea: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Why It Matters: If your business logic directly creates or calls low-level implementations, you lock your core logic to those implementations. Inverting the dependency lets you swap implementations without touching core code.

In Practice: Instead of a UserService that directly instantiates a SqlUserRepository, inject an IUserRepository interface. At runtime, you pass in the concrete implementation. This is the foundation of dependency injection.

Warning Signs: Use of new keywords scattered across your business logic; tests that require a real database or API to run; high-level code importing low-level implementation files.

How They Work Together

The five principles are not isolated rules; they reinforce each other. SRP keeps classes focused. OCP ensures adding behavior does not require modifying existing classes. LSP makes sure derived types are safe. ISP prevents forcing a class to implement more than it should. And DIP ties everything together by ensuring high-level policies depend only on abstractions.

When you apply all five, you end up with a codebase that is easier to test, less fragile when requirements change, and far more pleasant to maintain.

What Follows

Each article in this series takes one principle and walks through it in depth. You will see common mistakes, anti-patterns, and concrete refactoring examples in both C# and TypeScript. The goal is not to memorize rules but to develop an intuition for when your code is starting to hurt, and how to fix it before the pain becomes severe.

5 articles

About

Dev Patterns & Practices is a space for long-form thinking on design, technology, and craft. Every piece is written with care and the belief that the best ideas deserve room to breathe.