← Back to Article List         
SOLID Principles

SOLID Principles

Published on 28 Sep 2026     11 min read SOLID Principles
SOLID Principles

SOLID Principles

1. What is SOLID?

SOLID is a set of five object-oriented design principles used to create software that is:

  • Easy to understand
  • Easy to maintain
  • Easy to extend
  • Easy to test
  • Less tightly coupled
  • More flexible when requirements change

SOLID is especially important in large applications such as ASP.NET Core Web API, MVC, enterprise applications, Clean Architecture, and microservices.

SOLID stands for:

Letter Principle Main idea
S Single Responsibility Principle One class → one responsibility
O Open/Closed Principle Extend behavior without modifying existing code
L Liskov Substitution Principle Child types must safely replace parent types
I Interface Segregation Principle Prefer small, focused interfaces
D Dependency Inversion Principle Depend on abstractions, not concrete classes

Why do we need SOLID?

Consider a large application with hundreds of classes.

Without good design principles, you can easily end up with:

Small requirement change
        ↓
Modify one class
        ↓
Another feature breaks
        ↓
Modify several dependent classes
        ↓
More testing
        ↓
More bugs

SOLID tries to reduce this problem.

The basic goal is:

A change in one part of the application should have minimum impact on unrelated parts.

SOLID doesn't mean creating an interface and separate class for everything. It provides design guidelines for deciding how responsibilities and dependencies should be structured.


S — Single Responsibility Principle (SRP)

What is SRP?

Single Responsibility Principle says:

A class should have only one reason to change.

A simpler way to remember it:

One class should focus on one responsibility.

This does not literally mean that a class can contain only one method.

A class may have many methods as long as those methods belong to the same responsibility.


Why do we need SRP?

Suppose we have:

OrderService

and it is responsible for:

OrderService
 ├── Validate Order
 ├── Calculate Total
 ├── Save Order to Database
 ├── Send Email
 ├── Generate PDF
 └── Write Log

This class has too many reasons to change.

For example:

  • Database changes → modify OrderService
  • Email provider changes → modify OrderService
  • PDF format changes → modify OrderService
  • Business calculation changes → modify OrderService

That makes the class harder to maintain.

SRP suggests separating these responsibilities:

OrderService
    → Order processing

OrderRepository
    → Database operations

EmailService
    → Email operations

InvoiceService
    → Invoice/PDF operations

LoggingService
    → Logging

Now each component has a more focused responsibility.


Important clarification

SRP is often described as:

"A class should do only one thing."

That is useful for remembering the principle, but the more accurate definition is:

A class should have one reason to change.

For example:

EmployeeService
 ├── CreateEmployee()
 ├── UpdateEmployee()
 ├── DeleteEmployee()
 └── GetEmployee()

This does not automatically violate SRP.

All these operations can belong to the same responsibility:

Employee management

Advantages of SRP

  • Smaller and cleaner classes
  • Easier maintenance
  • Easier unit testing
  • Easier debugging
  • Changes are more isolated
  • Less chance of breaking unrelated functionality
  • Better separation of concerns

SRP Key Point

One class → one responsibility → one primary reason to change.


O — Open/Closed Principle (OCP)

What is OCP?

Open/Closed Principle says:

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

At first, this sounds contradictory.

It simply means:

Open for extension
        ↓
New behavior can be added

Closed for modification
        ↓
Existing stable code should not need repeated changes

Why do we need OCP?

Imagine a notification system:

NotificationService
 ├── Email
 └── SMS

Later the customer asks:

Add WhatsApp

You modify the existing service.

Later:

Add Push Notification

Again modify it.

Later:

Add Teams Notification

Again modify it.

The problem is that every new requirement forces us to modify already working code.

Every modification creates a risk of introducing regression bugs.

OCP encourages a design where new behavior can be introduced by adding new implementations, rather than repeatedly changing existing business logic.

Conceptually:

        INotification
             ↑
     ┌───────┼────────┐
     │       │        │
   Email    SMS    WhatsApp

Later:

        INotification
             ↑
     ┌───────┼──────────┬────────┐
     │       │          │        │
   Email    SMS      WhatsApp   Push

The new capability is introduced primarily through extension.


How is OCP normally achieved?

Common techniques include:

  • Interfaces
  • Abstract classes
  • Polymorphism
  • Composition
  • Dependency Injection
  • Strategy Pattern
  • Factory Pattern
  • Decorator Pattern

But simply using an interface does not automatically mean the code follows OCP. The overall design must allow new behavior without requiring repeated modifications to stable code.


Does "closed for modification" mean never modify existing code?

No.

Existing code will obviously need modification for:

  • Bug fixes
  • Refactoring
  • Requirement changes
  • Security fixes

OCP is about designing expected variations so that new variants don't require repeatedly rewriting stable logic.


Advantages of OCP

  • Easier feature expansion
  • Lower regression risk
  • Better extensibility
  • Existing tested code remains more stable
  • Supports plug-in style architecture
  • Works well with polymorphism and DI

OCP Key Point

Add new behavior primarily by extending the design rather than repeatedly changing stable code.


L — Liskov Substitution Principle (LSP)

What is LSP?

Liskov Substitution Principle says:

An object of a derived type should be usable wherever its base type is expected without breaking the expected behavior.

Simplified:

A child class must behave correctly when used as its parent type.

This principle is mainly concerned with inheritance and substitutability.


Simple Concept

Suppose:

Vehicle
   ↑
   |
Car

If the system expects a Vehicle, passing a Car should work correctly.

Vehicle expected
       ↑
       |
      Car
       ✓

The application shouldn't suddenly fail merely because a derived implementation was substituted.


Why do we need LSP?

Inheritance represents an is-a relationship.

For example:

Car is a Vehicle

But inheritance isn't correct merely because two objects appear related.

Suppose we design:

Bird
 └── Fly()

Then:

Bird
 ↑
 ├── Sparrow
 └── Penguin

The problem is:

Sparrow.Fly()   ✓

Penguin.Fly()   ✗

A penguin is biologically a bird, but it cannot satisfy the behavioral contract implied by a base Bird that requires every bird to fly.

If Penguin.Fly() throws something such as:

NotSupportedException

then code that expects every Bird to support Fly() can fail.

The abstraction itself is poorly designed.

A better conceptual model could separate capabilities:

Bird
 ↑
 ├── Sparrow
 └── Penguin

IFlyingBird
 ↑
 └── Sparrow

Now only objects that can actually provide flying behavior implement that capability.


LSP is about behavior, not just compilation

This is important.

The following relationship may compile:

Parent
  ↑
Child

But that doesn't prove LSP is satisfied.

The child must respect the behavioral expectations of the parent.

For example, it should not unexpectedly:

  • Reject valid operations supported by the parent contract
  • Produce incompatible results
  • Throw unsupported-operation exceptions for required behavior
  • Introduce stronger requirements that callers didn't need before
  • Violate guarantees made by the abstraction

Advantages of LSP

  • Safer inheritance
  • Reliable polymorphism
  • Fewer runtime surprises
  • Better abstractions
  • Derived classes behave consistently
  • Easier testing
  • Encourages proper inheritance hierarchies

LSP Key Point

If B is a subtype of A, B should safely work wherever A is expected.


I — Interface Segregation Principle (ISP)

What is ISP?

Interface Segregation Principle says:

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

Simplified:

Prefer small, focused interfaces instead of one large interface containing unrelated operations.


Why do we need ISP?

Imagine:

IEmployee
 ├── Work()
 ├── CalculateSalary()
 ├── ApproveLeave()
 ├── GenerateReport()
 ├── ManageEmployees()
 └── ConductInterview()

Now suppose we have:

Developer

The developer may need:

Work()
CalculateSalary()

but not:

ApproveLeave()
ManageEmployees()
ConductInterview()

If the class is forced to implement all of them, the interface is too broad.

This is sometimes called a fat interface.

ISP suggests splitting it into focused interfaces:

IWorker
 └── Work()

IPayable
 └── CalculateSalary()

ILeaveApprover
 └── ApproveLeave()

IReportGenerator
 └── GenerateReport()

IRecruiter
 └── ConductInterview()

A class implements only the capabilities it actually needs.


Another way to understand ISP

Without ISP

              IEmployee
                  |
     ┌────────────┼────────────┐
     ↓            ↓            ↓
 Developer      Manager       HR

Everyone is forced to depend on
all interface members.

With ISP

Developer
   ↓
IWorker


Manager
   ↓
IWorker
ILeaveApprover


HR
   ↓
IRecruiter

Dependencies become more precise.


Why are large interfaces problematic?

Suppose:

IService

contains 20 methods.

A particular class requires only two of them.

It is still coupled to the entire interface contract.

Changes to unrelated parts of that interface can affect implementations and consumers unnecessarily.

ISP reduces this unnecessary coupling.


Advantages of ISP

  • Smaller interfaces
  • Less unnecessary dependency
  • Cleaner implementations
  • Better maintainability
  • Easier testing and mocking
  • Classes implement only relevant capabilities
  • Reduces empty/dummy implementations
  • Encourages cohesive contracts

ISP Key Point

Don't force a class or client to depend on interface members it doesn't need.


D — Dependency Inversion Principle (DIP)

What is DIP?

Dependency Inversion Principle says:

High-level modules should not depend directly on low-level modules. Both should depend on abstractions.

Also:

Abstractions should not depend on implementation details; implementation details should depend on abstractions.

In simple terms:

Depend on interfaces/abstractions rather than directly coupling business logic to concrete implementations.


Why do we need DIP?

Imagine:

OrderService
     ↓
SqlOrderRepository

OrderService directly depends on a particular database implementation.

Later you want:

MongoOrderRepository

or:

ApiOrderRepository

The business service may need changes because it is tightly coupled to SqlOrderRepository.

Instead:

OrderService
     ↓
IOrderRepository
     ↑
     |
SqlOrderRepository

Now the high-level business logic knows the abstraction:

IOrderRepository

not the implementation detail:

SqlOrderRepository

Later:

              IOrderRepository
                    ↑
           ┌────────┼─────────┐
           │        │         │
          SQL     Mongo      API

The business layer remains much less coupled to the storage technology.


High-Level vs Low-Level Modules

This terminology is important for interviews.

High-level module

Contains important application/business decisions.

Examples:

OrderService
PaymentService
EmployeeService
CheckoutService

Low-level module

Handles technical implementation details.

Examples:

SQL database
Email sender
File storage
Logging provider
External API client

DIP encourages:

High-Level
    ↓
Abstraction
    ↑
Low-Level

instead of:

High-Level
    ↓
Low-Level

DIP vs Dependency Injection

These two are related but not the same thing.

DIP Dependency Injection
Design principle Implementation technique
Says how dependencies should be designed Says how dependencies can be supplied
Encourages depending on abstractions DI container can create and inject dependencies
Part of SOLID Built into ASP.NET Core

For example, conceptually:

DIP
 ↓
OrderService depends on IOrderRepository

DI
 ↓
ASP.NET Core injects SqlOrderRepository
as the implementation of IOrderRepository

So Dependency Injection helps implement Dependency Inversion, but using DI alone does not automatically mean a design follows DIP.


Advantages of DIP

  • Loose coupling
  • Easier unit testing
  • Easier replacement of implementations
  • Better maintainability
  • Better extensibility
  • Supports Dependency Injection
  • Works well with Clean Architecture
  • Business logic is less dependent on infrastructure details

DIP Key Point

Business logic should depend on abstractions rather than concrete infrastructure implementations.


How All Five Principles Work Together

Consider an order-processing application.

                    Order System
                         │
       ┌─────────────────┼─────────────────┐
       ↓                 ↓                 ↓
 Order Service     Payment Service   Notification
       │                 │                 │
       ↓                 ↓                 ↓
 Repository        Payment Gateway      Email/SMS

SOLID helps organize it:

Principle Question to ask
SRP Does this class have too many responsibilities/reasons to change?
OCP Can I add a new variant without repeatedly changing stable code?
LSP Can implementations/subtypes safely substitute for the abstraction?
ISP Is this interface forcing consumers to depend on unnecessary members?
DIP Is high-level business logic tightly coupled to implementation details?

Easy Way to Remember SOLID

S → Separate responsibilities
O → Open to extension
L → Legitimate substitution
I → Interfaces should be focused
D → Depend on abstractions

Or think about it this way:

SOLID
 │
 ├── S → One responsibility
 │
 ├── O → Extend without repeatedly changing stable code
 │
 ├── L → Child safely replaces parent
 │
 ├── I → Small focused interfaces
 │
 └── D → Depend on abstractions

Advantages of SOLID Overall

1. Maintainability

Changes are more localized and easier to understand.

2. Extensibility

New functionality can often be introduced with fewer changes to existing code.

3. Testability

Smaller responsibilities and abstraction-based dependencies make unit testing easier.

4. Loose Coupling

Classes know less about concrete implementation details.

5. Reusability

Focused components can often be reused more easily.

6. Reduced Regression Risk

Changes are less likely to unexpectedly affect unrelated functionality.

7. Better Team Development

Clear boundaries make large codebases easier for multiple developers to work on.

8. Supports Modern Architecture

SOLID concepts align well with architectures and techniques such as:

  • Clean Architecture
  • Layered Architecture
  • Hexagonal Architecture
  • Dependency Injection
  • Repository pattern
  • Strategy pattern
  • Factory pattern
  • Microservices

Important Interview Key Points

  • SOLID is a set of five object-oriented design principles.
  • SOLID is primarily about maintainability, extensibility, testability, and managing coupling.
  • SRP: A class should have one primary reason to change.
  • OCP: Design software so expected new variants can be added mainly through extension rather than repeated modification.
  • LSP: A subtype should be able to replace its base type without violating the expected contract.
  • ISP: Don't force clients to depend on interface members they don't need.
  • DIP: High-level business logic should depend on abstractions rather than concrete implementation details.
  • DIP and Dependency Injection are not the same. DI is one technique that can help implement DIP.
  • SOLID principles are guidelines, not rigid rules. Over-applying them can create unnecessary interfaces, classes, and complexity.
  • In ASP.NET Core, interfaces + Dependency Injection + focused services are common mechanisms for applying several SOLID principles.

One-line revision

S — One responsibility
O — Extend without repeatedly modifying stable code
L — Subtypes must be safely substitutable
I — Keep interfaces focused
D — Depend on abstractions, not implementation details.