← Back to Articles         
SOLID Principles & OOP Design

SOLID Principles & OOP Design

Published on 12 Oct 2026     13 min read OOPs
SOLID Principles & OOP Design

1. 🔴 What are the SOLID principles, and why are they important?

SOLID is a set of five object-oriented design principles that help developers build maintainable, flexible, testable, and loosely coupled applications.

SOLID principles are particularly useful in enterprise applications, ASP.NET Core, and Clean Architecture.

Five SOLID principles

Principle

Full name

Main purpose

S

Single Responsibility Principle

One reason to change

O

Open/Closed Principle

Extend without modifying existing code

L

Liskov Substitution Principle

Derived types must preserve base contracts

I

Interface Segregation Principle

Prefer focused interfaces

D

Dependency Inversion Principle

Depend on abstractions

Real-world ASP.NET Core example

Consider an order-processing application.

OrderController
      |
      v
IOrderService
      |
      v
OrderService
      |
      +----> IOrderRepository
      |             |
      |             v
      |       SqlOrderRepository
      |
      +----> IEmailService
                    |
                    v
               EmailService

This design can apply SOLID principles:

  • SRP: OrderService handles order-processing responsibilities.

  • OCP: New notification implementations can be added without changing order-processing logic.

  • LSP: Implementations must honor their interface contracts.

  • ISP: Repository and notification contracts are separate.

  • DIP: OrderService depends on interfaces rather than concrete implementations.

Advantages:

  • Easier maintenance and testing.

  • Reduced coupling.

  • Improved code reusability.

  • Easier extension of functionality.

  • Better separation of responsibilities.

Interview key point: SOLID principles help create software that is easier to modify, extend, and test without unnecessarily affecting existing functionality.

 

2. 🔴 What is the Single Responsibility Principle (SRP)? Explain with a C# example.

Single Responsibility Principle states that a class should have only one reason to change.

A class should focus on one responsibility rather than handling unrelated business operations.

Without SRP – Bad design

public class EmployeeService
{
    public void SaveEmployee()
    {
        Console.WriteLine("Saving Employee");
    }

    public void SendEmail()
    {
        Console.WriteLine("Sending Email");
    }

    public void GenerateReport()
    {
        Console.WriteLine("Generating Report");
    }
}

Problem: The class handles database operations, email notifications, and reporting.

Changes to any of these responsibilities may require modifying EmployeeService.

With SRP – Good design

public class EmployeeRepository
{
    public void Save()
    {
        Console.WriteLine("Saving Employee");
    }
}

public class EmailService
{
    public void Send()
    {
        Console.WriteLine("Sending Email");
    }
}

public class ReportService
{
    public void Generate()
    {
        Console.WriteLine("Generating Report");
    }
}

Each class now handles a distinct responsibility.

Benefits

  • Easier unit testing.

  • Better maintainability.

  • Reduced impact of changes.

  • Easier debugging.

  • Improved separation of concerns.

Real-world example: In ASP.NET Core, controllers handle HTTP requests, application services coordinate business use cases, and repositories handle persistence.

Key point: SRP does not mean a class must contain only one method. It means the class should have one cohesive responsibility and one primary reason to change.

 

3. 🔴 What is the Open/Closed Principle (OCP)? How can you implement it using interfaces?

Open/Closed Principle states that software entities should be open for extension but closed for modification.

New functionality should ideally be introduced by adding new implementations rather than repeatedly modifying stable existing code.

Without OCP – Bad design

public class PaymentService
{
    public void Pay(string paymentType)
    {
        if (paymentType == "CreditCard")
        {
            Console.WriteLine("Credit Card Payment");
        }
        else if (paymentType == "UPI")
        {
            Console.WriteLine("UPI Payment");
        }
    }
}

Problem: Every new payment method requires modifying PaymentService.

With OCP – Good design

Step 1: Create an interface

public interface IPayment
{
    void Pay();
}

Step 2: Create implementations

public class CreditCardPayment : IPayment
{
    public void Pay()
    {
        Console.WriteLine("Credit Card Payment");
    }
}

public class UpiPayment : IPayment
{
    public void Pay()
    {
        Console.WriteLine("UPI Payment");
    }
}

Step 3: Use abstraction

public class PaymentService
{
    private readonly IPayment _payment;

    public PaymentService(IPayment payment)
    {
        _payment = payment;
    }

    public void ProcessPayment()
    {
        _payment.Pay();
    }
}

Usage:

var service = new PaymentService(new UpiPayment());

service.ProcessPayment();

Output:

UPI Payment

To introduce another payment method, create another IPayment implementation without changing PaymentService. The application may still need configuration or registration changes.

Key point: OCP can be implemented through interfaces, polymorphism, and design patterns such as Strategy.

 

4. 🔴 What is the Liskov Substitution Principle (LSP)?

Liskov Substitution Principle states that objects of a derived type should be usable wherever their base type is expected without breaking the expected behavior.

Derived classes must honor the behavioral contracts of their base classes.

Without LSP – Bad design

public class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("Flying");
    }
}

public class Penguin : Bird
{
    public override void Fly()
    {
        throw new NotSupportedException();
    }
}

Problem: The base class promises flying behavior, but Penguin cannot fulfill that contract.

Bird bird = new Penguin();

bird.Fly(); // Exception

This violates the expected behavior of Bird.

With LSP – Good design

public interface IBird
{
    void Eat();
}

public interface IFlyingBird : IBird
{
    void Fly();
}

Implementations:

public class Sparrow : IFlyingBird
{
    public void Eat()
    {
        Console.WriteLine("Eating");
    }

    public void Fly()
    {
        Console.WriteLine("Flying");
    }
}

public class Penguin : IBird
{
    public void Eat()
    {
        Console.WriteLine("Eating");
    }
}

Now only birds that support flying implement IFlyingBird.

Important LSP rules

  • Derived types must honor base-type expectations.

  • They should not strengthen preconditions unexpectedly.

  • They should not weaken promised postconditions.

  • They must preserve relevant invariants.

  • They should not introduce unexpected failures for supported operations.

Key point: Inheritance is not just about reusing code. Derived classes must preserve the behavioral contract of their base classes.

 

5. 🔴 What is the Interface Segregation Principle (ISP)?

Interface Segregation Principle states that clients should not be forced to depend on methods they do not use.

Instead of creating one large interface, divide it into smaller, focused interfaces based on client requirements.

Without ISP – Bad design

public interface IEmployee
{
    void Work();
    void Eat();
    void GenerateReport();
}

Every implementing class must provide all three methods, even when some are irrelevant.

With ISP – Good design

public interface IWorkable
{
    void Work();
}

public interface IEatable
{
    void Eat();
}

public interface IReportGenerator
{
    void GenerateReport();
}

Implementation:

public class Developer : IWorkable
{
    public void Work()
    {
        Console.WriteLine("Writing Code");
    }
}

public class Manager : IWorkable, IReportGenerator
{
    public void Work()
    {
        Console.WriteLine("Managing Team");
    }

    public void GenerateReport()
    {
        Console.WriteLine("Generating Report");
    }
}

Benefits

  • Smaller and focused interfaces.

  • Reduced unnecessary dependencies.

  • Easier mocking and unit testing.

  • Better maintainability.

  • Fewer unsupported operations.

Real-world example: Separate IReadRepository<T> and IWriteRepository<T> when consumers need different capabilities, rather than forcing every consumer to depend on a large repository interface.

Key point: Clients should depend only on the operations they actually require.

 

6. 🔴 What is the Dependency Inversion Principle (DIP)?

Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions.

Additionally, abstractions should not depend on implementation details; implementation details should depend on abstractions.

Without DIP – Tight coupling

public class SqlRepository
{
    public void Save()
    {
        Console.WriteLine("Saved to SQL Server");
    }
}

public class EmployeeService
{
    private readonly SqlRepository _repository = new();

    public void SaveEmployee()
    {
        _repository.Save();
    }
}

Problem: EmployeeService directly depends on SqlRepository.

Changing the persistence implementation may require modifying the service.

With DIP – Loose coupling

Step 1: Define abstraction

public interface IRepository
{
    void Save();
}

Step 2: Implement abstraction

public class SqlRepository : IRepository
{
    public void Save()
    {
        Console.WriteLine("Saved to SQL Server");
    }
}

Step 3: Depend on abstraction

public class EmployeeService
{
    private readonly IRepository _repository;

    public EmployeeService(IRepository repository)
    {
        _repository = repository;
    }

    public void SaveEmployee()
    {
        _repository.Save();
    }
}

ASP.NET Core registration:

builder.Services.AddScoped<IRepository, SqlRepository>();
builder.Services.AddScoped<EmployeeService>();

Dependency flow

EmployeeService
      |
      v
  IRepository
      ^
      |
SqlRepository

The high-level service and low-level repository implementation both depend on the abstraction.

Key point: DIP reduces coupling between business logic and implementation details. It is fundamental to Clean Architecture.

 

7. 🔴 What is the difference between Dependency Inversion and Dependency Injection?

Dependency Inversion Principle (DIP) is a design principle, while Dependency Injection (DI) is a technique used to supply dependencies to a class.

DI is commonly used to implement designs that follow DIP.

Example

DIP – Depend on interface

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
}

OrderService depends on the abstraction IOrderRepository.

DI – Supply the implementation

builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();

ASP.NET Core supplies the registered implementation when constructing OrderService.

Comparison

Dependency Inversion

Dependency Injection

SOLID design principle

Design pattern/technique

Defines dependency direction

Supplies dependencies

Promotes abstraction-based design

Reduces manual object creation

Does not require a DI container

Can use a DI container

Focuses on architecture

Focuses on object construction

Practical flow

DIP: Design the dependency
--------------------------

OrderService
     |
     v
IOrderRepository
     ^
     |
SqlOrderRepository


DI: Provide the implementation
------------------------------

ASP.NET Core DI Container
           |
           v
Create SqlOrderRepository
           |
           v
Inject into OrderService

Important: DI and DIP are related but independent. Constructor injection can supply a concrete dependency without following DIP, and DIP can be followed without a DI container.

Key point: DIP tells us what to depend on; DI helps provide those dependencies.

 

8. 🟡 What is composition over inheritance, and why is it recommended?

Composition over inheritance is a design guideline that recommends building functionality by combining objects rather than relying unnecessarily on inheritance hierarchies.

Composition represents a HAS-A relationship, while inheritance represents an IS-A relationship.

Inheritance example

public class Engine
{
    public void Start()
    {
        Console.WriteLine("Engine Started");
    }
}

public class Car : Engine
{
}

Problem: A car is not an engine. A car has an engine.

Composition example

public class Engine
{
    public void Start()
    {
        Console.WriteLine("Engine Started");
    }
}

public class Car
{
    private readonly Engine _engine;

    public Car(Engine engine)
    {
        _engine = engine;
    }

    public void StartCar()
    {
        _engine.Start();
    }
}

Usage:

Car car = new Car(new Engine());

car.StartCar();

Comparison

Inheritance

Composition

IS-A relationship

HAS-A relationship

Reuses behavior through base classes

Reuses behavior through contained objects

Stronger structural coupling

Often more flexible

Changes to base behavior can affect derived classes

Components can be replaced independently

Useful for true subtype relationships

Useful for combining behaviors

Real-world ASP.NET Core example

public class OrderService
{
    private readonly IEmailService _emailService;

    public OrderService(IEmailService emailService)
    {
        _emailService = emailService;
    }
}

OrderService uses an email service rather than inheriting from it.

Key point: Prefer composition for combining independent behaviors. Use inheritance when a genuine subtype relationship exists and the LSP contract can be maintained.

 

9. 🟡 What is the difference between association, aggregation, and composition?

These concepts describe relationships between objects in object-oriented design.

Association – General relationship

Association means two objects are related or interact with each other.

Example: A teacher teaches a student.

public class Student
{
    public string Name { get; set; } = "";
}

public class Teacher
{
    public void Teach(Student student)
    {
        Console.WriteLine(student.Name);
    }
}

Both objects can exist independently.

Aggregation – Weak HAS-A relationship

Aggregation represents a whole-part relationship where the part can exist independently of the whole.

Example: A department has employees.

public class Employee
{
    public string Name { get; set; } = "";
}

public class Department
{
    public List<Employee> Employees { get; }

    public Department(List<Employee> employees)
    {
        Employees = employees;
    }
}

Employees can exist independently of the department and may be associated with another department.

Composition – Strong HAS-A relationship

Composition represents a whole-part relationship with strong ownership, where the part's logical lifecycle is controlled by the whole.

Example: An order owns its order items.

public class OrderItem
{
    public string Product { get; }

    public OrderItem(string product)
    {
        Product = product;
    }
}

public class Order
{
    private readonly List<OrderItem> _items = new();

    public void AddItem(string product)
    {
        _items.Add(new OrderItem(product));
    }
}

Here, the order controls the creation and ownership of its order items.

In C#, composition is a design relationship; it does not automatically guarantee that the CLR destroys contained objects when the parent becomes unreachable.

Comparison

Association

Aggregation

Composition

General relationship

Weak whole-part relationship

Strong whole-part relationship

Objects interact

Whole contains independent parts

Whole owns parts

Independent lifecycles

Parts can outlive whole

Parts logically depend on whole

Teacher–Student

Department–Employee

Order–OrderItem

UML: plain line

UML: hollow diamond

UML: filled diamond

UML-style representation

Association
Teacher ---------- Student

Aggregation
Department ◇------ Employee

Composition
Order ◆------------ OrderItem

Key point: Association means objects are related; aggregation means weak ownership; composition means strong ownership and lifecycle dependency.

 

10. 🟡 How do SOLID principles improve testability and maintainability?

SOLID principles improve software quality by reducing unnecessary dependencies and separating responsibilities.

They allow developers to modify or test one part of an application without requiring changes throughout the system.

Example: Without SOLID

public class OrderService
{
    private readonly SqlRepository _repository = new();

    public void PlaceOrder()
    {
        _repository.Save();
    }
}

Problem: The service creates a concrete database dependency, making isolated unit testing difficult.

Example: With SOLID

public interface IOrderRepository
{
    void Save();
}

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }

    public void PlaceOrder()
    {
        _repository.Save();
    }
}

Unit testing with a fake repository

public class FakeOrderRepository : IOrderRepository
{
    public bool SaveCalled { get; private set; }

    public void Save()
    {
        SaveCalled = true;
    }
}

Test example:

var fakeRepository = new FakeOrderRepository();

var service = new OrderService(fakeRepository);

service.PlaceOrder();

Console.WriteLine(fakeRepository.SaveCalled); // True

No actual SQL Server connection is required.

How each SOLID principle helps

Principle

Testability and maintainability benefit

SRP

Tests focus on one responsibility

OCP

New functionality can be added with fewer changes to existing code

LSP

Implementations can be substituted reliably

ISP

Smaller interfaces are easier to mock

DIP

Dependencies can be replaced with test doubles

Key point: SOLID principles support modular design, reduce regression risk, and make unit testing easier. They are guidelines, not rules requiring an interface for every class.

 

Quick Interview Revision

Concept

Key point

SOLID

Five principles for maintainable OOP design

SRP

One reason to change

OCP

Open for extension, closed for modification

LSP

Derived types must preserve base contracts

ISP

Clients should not depend on unused methods

DIP

Depend on abstractions

DI

Inject dependencies from outside

Tight coupling

Direct dependency on concrete implementations

Loose coupling

Reduced dependency on implementation details

Composition

HAS-A relationship

Inheritance

IS-A relationship

Association

General object relationship

Aggregation

Weak whole-part ownership

Composition relationship

Strong whole-part ownership

Interview Priority – Senior .NET Developer / Technical Lead

Topic

Priority

All five SOLID principles with examples

🔴 Must know

SRP in ASP.NET Core services

🔴 Must know

OCP using interfaces and Strategy Pattern

🔴 Must know

LSP and inheritance design

🔴 Must know

ISP and repository interface design

🔴 Must know

DIP vs DI

🔴 Must know

SOLID in Clean Architecture

🔴 Must know

Composition over inheritance

🟡 Should know

Association, aggregation, composition

🟡 Should know

SOLID and unit testing

🔴 Must know

Important Technical Lead Interview Scenario

Question: How have you applied SOLID principles in your ASP.NET Core Clean Architecture project?

A suitable interview answer:

In ASP.NET Core Clean Architecture, I apply SRP by separating controllers, application services, and repositories. I use interfaces and Dependency Injection to implement DIP and reduce coupling between Application and Infrastructure. OCP allows new implementations without changing core business logic, while ISP keeps interfaces focused. LSP ensures that different implementations follow the same contracts. These practices improve maintainability, unit testing, and extensibility.

Remember: SOLID is not about creating more classes or interfaces. It is about designing clear responsibilities, stable contracts, and manageable dependencies.

← Back to Articles