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.