Forget the textbook definition for a moment.
If someone in an interview asks:
βWhat are SOLID principles?β
I don't want you to remember five fancy definitions.
I want you to look at your ASP.NET Core code and immediately think:
βThis class is doing too much.β
βThis code is tightly coupled.β
βAdding a new type will force me to modify existing code.β
βMy derived class can't actually behave like its base class.β
βThis interface is becoming a dumping ground.β
That is when you've actually learned SOLID.
Imagine you build an ASP.NET Core application.
Initially:
Orders API
β
OrderService
β
SQL Server
Very simple.
You write:
public class OrderService
{
public void CreateOrder(Order order)
{
// Save order
}
public void SendEmail(Order order)
{
// Send email
}
public void GenerateInvoice(Order order)
{
// Generate PDF
}
public void SendSms(Order order)
{
// Send SMS
}
}
Everything works.
Then business grows.
Suddenly they say:
"We need WhatsApp notifications."
Then:
"We need Azure Blob Storage."
Then:
"We need Stripe."
Then:
"We need another payment provider."
Then:
"We need to change SQL Server implementation."
Your OrderService becomes:
OrderService
βββ Database
βββ Email
βββ SMS
βββ WhatsApp
βββ PDF
βββ Payment
βββ Logging
βββ File storage
βββ Validation
βββ probably your mental health
π
This is exactly the kind of mess SOLID tries to prevent.
S β Single Responsibility Principle
O β Open/Closed Principle
L β Liskov Substitution Principle
I β Interface Segregation Principle
D β Dependency Inversion Principle
Don't memorize these yet.
We're going to build an ASP.NET Core application and discover why each principle exists.
A class should have one main job.
Not:
"A class should have only one method."
No.
It means:
A class should have one reason to change.
This sentence is VERY important.
Imagine you hire one employee.
His job:
Employee
βββ Cook food
βββ Clean restaurant
βββ Handle payments
βββ Deliver food
βββ Repair AC
βββ Manage Instagram
Technically...
He CAN do all of it.
But should he?
π
No.
If Instagram requirements change, why should the person responsible for cooking need to change?
That's the problem.
Suppose we have an order API.
public class OrderService
{
public void CreateOrder(Order order)
{
// Validate order
// Save to database
// Send email
// Generate invoice
// Send SMS
}
}
Looks convenient.
But this class has MANY responsibilities.
OrderService
|
+-- Validation
|
+-- Database
|
+-- Email
|
+-- Invoice
|
+-- SMS
Now imagine:
Email provider changes.
You modify OrderService.
Then:
Database changes.
You modify OrderService.
Then:
Invoice format changes.
You modify OrderService.
That's a problem.
Split responsibilities.
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
private readonly IInvoiceService _invoiceService;
public OrderService(
IOrderRepository repository,
IEmailService emailService,
IInvoiceService invoiceService)
{
_repository = repository;
_emailService = emailService;
_invoiceService = invoiceService;
}
public async Task CreateOrder(Order order)
{
await _repository.Save(order);
await _invoiceService.Generate(order);
await _emailService.SendOrderConfirmation(order);
}
}
Now:
OrderService
|
+---- IOrderRepository
|
+---- IInvoiceService
|
+---- IEmailService
Each component has a focused responsibility.
Suppose tomorrow email changes.
Old:
OrderService
β
Change OrderService
New:
OrderService
β
IEmailService
β
Change EmailService implementation
OrderService doesn't care.
Think:
S = Single Job
Like a restaurant:
Chef β cooks
Cashier β handles payment
Waiter β serves
Cleaner β cleans
Don't make the chef also repair the AC.
This one sounds scary.
It's actually very simple.
Your code should be open for extension but closed for modification.
Meaning:
When a new requirement comes, try to add new code instead of constantly modifying old working code.
Imagine a payment system.
Today:
Payment
βββ Credit Card
βββ UPI
Tomorrow:
Payment
βββ Credit Card
βββ UPI
βββ PayPal
βββ Stripe
βββ Razorpay
You write:
public class PaymentService
{
public void Pay(string paymentType)
{
if (paymentType == "CreditCard")
{
// Credit card payment
}
else if (paymentType == "UPI")
{
// UPI payment
}
else if (paymentType == "PayPal")
{
// PayPal payment
}
}
}
Now business says:
Add Razorpay.
You modify this class.
Then:
Add Stripe.
Modify again.
Then:
Add Apple Pay.
Modify again.
Eventually:
if (...)
{
}
else if (...)
{
}
else if (...)
{
}
else if (...)
{
}
else if (...)
{
}
else if (...)
{
}
π
Create an abstraction:
public interface IPaymentProcessor
{
Task Pay(decimal amount);
}
Then implementations:
public class CreditCardPayment : IPaymentProcessor
{
public Task Pay(decimal amount)
{
// Credit card logic
return Task.CompletedTask;
}
}
public class UpiPayment : IPaymentProcessor
{
public Task Pay(decimal amount)
{
// UPI logic
return Task.CompletedTask;
}
}
public class RazorpayPayment : IPaymentProcessor
{
public Task Pay(decimal amount)
{
// Razorpay logic
return Task.CompletedTask;
}
}
Now your main service doesn't need to know every payment type.
public class PaymentService
{
private readonly IPaymentProcessor _processor;
public PaymentService(IPaymentProcessor processor)
{
_processor = processor;
}
public Task Process(decimal amount)
{
return _processor.Pay(amount);
}
}
New payment provider?
Create:
public class StripePayment : IPaymentProcessor
{
public Task Pay(decimal amount)
{
// Stripe
return Task.CompletedTask;
}
}
We extended the system.
We didn't modify the existing payment implementations.
Think:
O = Add, don't constantly break.
Or:
Old working code should ideally remain untouched when adding a new behavior.
Real-world analogy:
You bought a phone.
You want a new capability.
Would you manufacture a completely new phone every time?
No.
You plug in an accessory.
Existing system
β
Extension
β
New behavior
This is the one that scares developers.
Don't worry.
I'll make it stupidly simple.
The rule:
If B is a subtype of A, you should be able to use B wherever A is expected without breaking the application.
In human language:
Child should behave like the parent promises.
Imagine:
Bird
β
Penguin
You create:
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("Flying");
}
}
Then:
public class Penguin : Bird
{
public override void Fly()
{
throw new Exception("I can't fly!");
}
}
Oops.
We said:
Penguin IS-A Bird
But our Bird contract says:
Every Bird can fly.
Penguin breaks that assumption.
Imagine:
public void MakeBirdFly(Bird bird)
{
bird.Fly();
}
Works:
MakeBirdFly(new Eagle());
But:
MakeBirdFly(new Penguin());
π₯
Exception.
Our base abstraction was wrong.
Imagine:
public interface IFileStorage
{
Task Save(string file);
Task Delete(string file);
}
You implement:
public class LocalFileStorage : IFileStorage
{
public Task Save(string file)
{
// Save file
return Task.CompletedTask;
}
public Task Delete(string file)
{
// Delete file
return Task.CompletedTask;
}
}
Fine.
Then someone creates:
public class ReadOnlyFileStorage : IFileStorage
{
public Task Save(string file)
{
throw new NotSupportedException();
}
public Task Delete(string file)
{
throw new NotSupportedException();
}
}
Now code expecting:
IFileStorage storage
assumes:
storage.Save(...)
storage.Delete(...)
But one implementation explodes.
That's a sign the abstraction doesn't represent a valid common contract.
LSP isn't simply:
"Don't throw exceptions."
It's:
Don't create an abstraction that promises behavior which some implementations cannot honor.
This is a HUGE architecture lesson.
Think:
L = Like the parent promised
If I give you an IFileStorage, you should be able to use any valid implementation without suddenly discovering:
"Oh sorry bro, this implementation doesn't support half of the interface."
Big fancy sentence:
Clients should not be forced to depend on methods they don't use.
Dumb version:
Don't create one giant interface containing everything.
Imagine this interface:
public interface IEmployee
{
void Work();
void Eat();
void Drive();
void Code();
void Cook();
void ManagePeople();
}
Now you have a chef.
Does chef need:
Code()
No.
Does every employee need:
Cook()
No.
Your interface is becoming a giant bucket.
β Bad:
public interface IUserService
{
User GetUser(int id);
void CreateUser(User user);
void DeleteUser(int id);
void SendEmail(User user);
void ResetPassword(User user);
void GenerateReport();
void ExportToExcel();
}
Imagine another class only needs:
GetUser()
Why should it depend on all these unrelated methods?
Split interfaces based on responsibilities.
public interface IUserReader
{
User GetUser(int id);
}
public interface IUserWriter
{
void CreateUser(User user);
void DeleteUser(int id);
}
public interface IPasswordService
{
void ResetPassword(User user);
}
public interface IUserReportService
{
void GenerateReport();
}
Now consumers depend only on what they need.
Imagine a restaurant gives you a giant remote:
Remote
--------------------------------
TV
AC
Lights
Fan
Washing Machine
Garage
Oven
Security System
Coffee Machine
...
Why?
π
Give me the remote for the thing I need.
Same with interfaces.
I = Interfaces should be small and focused.
Or:
Don't force someone to carry a 20 kg toolbox when they only need a screwdriver.
This is probably the most important SOLID principle for ASP.NET Core developers.
And guess what?
You already use it every day.
It's the reason Dependency Injection exists.
Imagine:
public class OrderService
{
private SqlOrderRepository _repository;
public OrderService()
{
_repository = new SqlOrderRepository();
}
}
OrderService is directly dependent on:
SqlOrderRepository
Meaning:
OrderService
β
SqlOrderRepository
β
SQL Server
Tomorrow you want:
MongoOrderRepository
Uh oh.
OrderService is tightly connected to SQL.
Your business logic knows too much about infrastructure.
Business Logic
β
Concrete Infrastructure
That's backwards.
Your business logic should depend on an abstraction.
Create:
public interface IOrderRepository
{
Task<Order?> GetById(int id);
Task Save(Order order);
}
Then:
public class SqlOrderRepository : IOrderRepository
{
public Task<Order?> GetById(int id)
{
// SQL Server
return Task.FromResult<Order?>(null);
}
public Task Save(Order order)
{
// SQL Server
return Task.CompletedTask;
}
}
Now:
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public async Task CreateOrder(Order order)
{
await _repository.Save(order);
}
}
Look at the difference.
OrderService
β
SqlOrderRepository
β
SQL Server
IOrderRepository
β β
| |
| |
OrderService SqlOrderRepository
The business logic doesn't care whether the implementation uses:
SQL Server
MongoDB
DynamoDB
API
Mock
In-memory DB
In Program.cs:
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
Now ASP.NET Core says:
"Whenever somebody asks me for
IOrderRepository, I'll give themSqlOrderRepository."
So:
public OrderService(IOrderRepository repository)
gets:
SqlOrderRepository
automatically.
That's Dependency Injection.
D = Depend on abstraction, not concrete implementation.
Or even simpler:
Don't marry the implementation. Date the interface. π
Imagine we're building:
ASP.NET Core API
|
β
OrderService
|
+------------+-------------+
| | |
β β β
Repository Payment Notification
| | |
β β β
SQL DB Stripe Email
SOLID helps us keep this architecture flexible.
Each component has one main responsibility.
OrderService
β
Order business logic
Repository
β
Data access
PaymentService
β
Payment
EmailService
β
Email
Want a new payment provider?
Don't destroy existing code.
IPaymentProcessor
|
+---+----+
| |
Stripe Razorpay
Add another implementation.
Every implementation must honor the abstraction.
IPaymentProcessor
|
+---+------+
| |
Stripe Razorpay
If the system expects:
processor.Pay()
both should actually support that behavior.
Don't create:
IGodService
βββ Payment
βββ Email
βββ User
βββ Reports
βββ Files
βββ Orders
βββ Everything else
Instead:
IOrderRepository
IPaymentProcessor
IEmailService
IFileStorage
Small interfaces.
Business logic depends on:
Interfaces
not:
Concrete infrastructure
OrderService
β
IOrderRepository
β
SqlOrderRepository
Let's build a tiny order system.
public interface IOrderRepository
{
Task Save(Order order);
}
Implementation:
public class SqlOrderRepository : IOrderRepository
{
public async Task Save(Order order)
{
// EF Core / SQL Server
}
}
public interface IPaymentProcessor
{
Task ProcessPayment(decimal amount);
}
Implementation:
public class StripePaymentProcessor : IPaymentProcessor
{
public async Task ProcessPayment(decimal amount)
{
// Stripe API
}
}
Another implementation:
public class RazorpayPaymentProcessor : IPaymentProcessor
{
public async Task ProcessPayment(decimal amount)
{
// Razorpay API
}
}
public interface INotificationService
{
Task SendOrderConfirmation(Order order);
}
Implementation:
public class EmailNotificationService : INotificationService
{
public async Task SendOrderConfirmation(Order order)
{
// Send email
}
}
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IPaymentProcessor _paymentProcessor;
private readonly INotificationService _notificationService;
public OrderService(
IOrderRepository repository,
IPaymentProcessor paymentProcessor,
INotificationService notificationService)
{
_repository = repository;
_paymentProcessor = paymentProcessor;
_notificationService = notificationService;
}
public async Task CreateOrder(Order order)
{
await _paymentProcessor.ProcessPayment(order.Amount);
await _repository.Save(order);
await _notificationService.SendOrderConfirmation(order);
}
}
Look at this class.
Does it know:
How SQL works? β
How Stripe works? β
How email works? β
It only knows:
"I need someone who can save orders."
"I need someone who can process payment."
"I need someone who can send notification."
That's beautiful.
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IPaymentProcessor, StripePaymentProcessor>();
builder.Services.AddScoped<INotificationService, EmailNotificationService>();
builder.Services.AddScoped<OrderService>();
ASP.NET Core wires everything together.
ASP.NET Core DI
|
+--------------+--------------+
| | |
β β β
IOrderRepository IPaymentProcessor INotificationService
β β β
SqlRepository Stripe Email
SOLID doesn't mean:
"Create an interface for every class."
No.
That's one of the biggest misunderstandings.
You don't do this:
UserService
β
IUserService
β
UserServiceImplementation
just because someone said:
"SOLID!"
π
That's pointless if the abstraction provides no useful boundary.
SOLID is about managing change and dependencies.
When reviewing your ASP.NET Core code, ask these five questions.
Is this class doing too many unrelated things?
If yes β probably SRP problem.
When I add a new behavior, do I keep modifying a giant
if/elseorswitch?
If yes β probably OCP problem.
Can I replace this implementation with another implementation without breaking the caller's expectations?
If no β probably LSP problem.
Is this interface forcing classes to implement methods they don't need?
If yes β probably ISP problem.
Does my business logic directly create or depend on infrastructure classes?
If yes β probably DIP problem.
Remember a restaurant.
Chef β Cooking
Cashier β Payment
Waiter β Serving
One person = one main job.
Restaurant
β
Add new dish
Don't rebuild the entire restaurant.
Extend instead of constantly modifying old code.
"Pizza"
β
Any valid pizza should behave like a pizza.
If you order a pizza and receive a brick...
π
The abstraction is lying.
Child/implementation must honor the parent's promise.
Don't give the waiter a giant remote:
TV
AC
Washing Machine
Garage
Oven
Lights
...
Give small, focused controls.
Small interfaces.
Restaurant doesn't say:
"I ONLY accept Visa machine XYZ."
It says:
"I need something that can process payment."
Restaurant
β
Payment Interface
β
Visa
MasterCard
UPI
Stripe
Depend on abstraction, not implementation.
When you see an ASP.NET Core application:
API Controller
|
β
OrderService
|
+-------------+-------------+
| | |
β β β
IRepository IPayment INotification
| | |
β β β
SQL Server Stripe Email
Think:
S β Each class has one main job.
O β Add new behavior without constantly modifying old code.
L β Implementations must honor the abstraction's promise.
I β Keep interfaces small and focused.
D β Business logic depends on abstractions, not concrete implementations.
And if you remember only one sentence for each:
π’ S β One class, one main job.
π΅ O β Add new behavior without breaking/modifying old behavior unnecessarily.
π‘ L β A replacement implementation should not break what the caller expects.
π£ I β Don't force clients to depend on methods they don't need.
π΄ D β Depend on abstractions, not concrete implementations.
SOLID is not about writing more interfaces.
It's about making this:
"I changed ONE requirement."
β
"Why did 17 classes break?"
become this:
"I changed ONE requirement."
β
"I changed ONE small part."
β
"Everything else is still safe."
That's the real reason senior developers care about SOLID.
If you have only 5 minutes before an interview, revise this section.
S β Single Responsibility
O β Open/Closed
L β Liskov Substitution
I β Interface Segregation
D β Dependency Inversion
Think:
SOLID = Make code easier to change without breaking everything.
A class should have one responsibility and one reason to change.
One class = One main job
OrderService
βββ Database
βββ Email
βββ SMS
βββ Invoice
βββ Validation
OrderService
Repository
EmailService
SmsService
InvoiceService
Validator
"Does this class have too many unrelated reasons to change?"
S = Single Job
Software entities should be open for extension but closed for modification.
Add new behavior without constantly modifying working code.
if (type == "Stripe")
{
}
else if (type == "Razorpay")
{
}
else if (type == "PayPal")
{
}
public interface IPaymentProcessor
{
Task Pay(decimal amount);
}
Then:
IPaymentProcessor
|
+---+-------+
| |
Stripe Razorpay
Want another provider?
Add new class.
"Will adding a new behavior force me to modify a giant existing class?"
O = Open to Extension, Closed to unnecessary Modification
Subtypes should be substitutable for their base types without breaking expected behavior.
Child must honor what the parent promises.
public class Bird
{
public virtual void Fly()
{
}
}
public class Penguin : Bird
{
public override void Fly()
{
throw new Exception();
}
}
The abstraction says:
Bird β can Fly
But:
Penguin β cannot Fly
So the abstraction is wrong.
"Can I replace one implementation with another without breaking the caller's expectations?"
L = Like the parent promised
Clients should not be forced to depend on methods they do not use.
Don't create giant interfaces.
public interface IUserService
{
User GetUser(int id);
void CreateUser(User user);
void DeleteUser(int id);
void SendEmail(User user);
void ResetPassword(User user);
void GenerateReport();
void ExportToExcel();
}
IUserReader
IUserWriter
IPasswordService
IUserReportService
"Is this interface forcing a class to implement things it doesn't need?"
I = Interfaces should be small
High-level modules should not depend directly on low-level modules. Both should depend on abstractions.
Business logic should depend on interfaces, not concrete infrastructure.
public class OrderService
{
private SqlOrderRepository _repository;
public OrderService()
{
_repository = new SqlOrderRepository();
}
}
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
}
And:
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
"Does my business logic directly create or depend on infrastructure?"
D = Depend on abstraction, not implementation
| Principle | Simple Meaning | Smell |
|---|---|---|
| S | One class, one main job | Giant class |
| O | Extend without unnecessary modification | Giant if/else/switch |
| L | Replacement must honor the contract | Child breaks parent expectations |
| I | Small focused interfaces | Giant interface |
| D | Depend on abstractions | new ConcreteClass() inside business logic |
When you review code, ask:
S β Is this class doing too much?
O β Do I have to modify old code for every new behavior?
L β Can implementations safely replace each other?
I β Is the interface too large?
D β Does business logic depend on concrete infrastructure?
If you can answer these five questions, you can usually identify the SOLID problem.
Remember this architecture:
API Controller
|
β
OrderService
|
+-------------+-------------+
| | |
β β β
IRepository IPayment INotification
| | |
β β β
SQL Server Stripe Email
And think:
Controller
β
Business Logic
β
Abstractions
β
Infrastructure
The business layer should not care whether infrastructure uses:
SQL Server
MongoDB
DynamoDB
Stripe
Razorpay
SMTP
SendGrid
Azure Blob
AWS S3
It should care about what capability it needs, not how that capability is implemented.
This is a common interview trap.
A design principle:
Depend on abstractions.
Don't depend directly on concrete implementations.
A technique/mechanism used to provide dependencies:
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
ASP.NET Core's DI container supplies the implementation:
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
DIP = Principle
DI = Technique
Don't say these in an interview:
β Every class must have an interface.
β Every method needs its own class.
β More classes automatically means better SOLID.
β Every if/else violates OCP.
β Dependency Injection and Dependency Inversion are the same thing.
Instead:
SOLID is about designing boundaries so that changes remain localized and dependencies remain manageable.
If the interviewer says:
"Explain SOLID."
Say:
"SOLID is a group of five object-oriented design principles that help us create maintainable, flexible, and change-friendly software.
S is Single Responsibility β a class should have one main responsibility.
O is Open/Closed β we should be able to extend behavior without unnecessarily modifying existing working code.
L is Liskov Substitution β derived implementations should honor the behavior expected from the abstraction.
I is Interface Segregation β clients shouldn't be forced to depend on methods they don't need.
D is Dependency Inversion β high-level business logic should depend on abstractions rather than concrete infrastructure.
In ASP.NET Core, we commonly apply these through services, interfaces, dependency injection, repositories, and separate infrastructure implementations."
Answer:
Dependency Inversion Principle.
But be precise:
DIP = principle
DI = implementation technique
Two can come up:
ISP β Don't create interfaces that are too large.
DIP β Consumers should depend on abstractions.
Don't confuse them.
Use this:
OrderController
β
OrderService
β
+--------------------+
| Abstractions |
| |
| IOrderRepository |
| IPaymentProcessor |
| INotification |
+--------------------+
β
+--------------------+
| Implementations |
| |
| SQL Repository |
| Stripe |
| Email |
+--------------------+
Then explain:
S β Responsibilities are separated.
O β New payment providers can be added.
L β Implementations honor their interfaces.
I β Interfaces are focused.
D β OrderService depends on interfaces.
Memorize this:
S β ONE JOB
O β ADD, DON'T BREAK
L β KEEP THE PROMISE
I β SMALL INTERFACES
D β DEPEND ON ABSTRACTIONS
That's your SOLID emergency revision.
When you see bad code:
One giant class
β
Lots of responsibilities
β
Lots of if/else
β
Giant interfaces
β
Concrete dependencies
β
Changing one thing breaks many things
Think:
SOLID
β
Separate responsibilities
β
Create useful abstractions
β
Keep interfaces focused
β
Allow implementations to vary safely
β
Inject dependencies
β
Localize change
SOLID is about designing code so that when one business requirement changes, you don't have to change half the application.