๐งฉ SOLID Principles Deep Dive (I & D): Let's Learn from ZERO!
We've reached the final two principles of SOLID! ๐ In this post we'll understand Interface Segregation (I) and Dependency Inversion (D) from absolute zero, with Python examples, real-world scenarios, FastAPI applications and interview questions.
๐ค The Five SOLID Principles
| Letter | Principle | One-line idea |
|---|---|---|
| S | Single Responsibility | A class should have one responsibility |
| O | Open/Closed | Open for extension, closed for modification |
| L | Liskov Substitution | Child classes should be usable wherever their parent is expected |
| I โญ | Interface Segregation | Don't force a class to implement methods it doesn't need |
| D โญ | Dependency Inversion | Depend on abstractions, not concrete implementations |
(Today's topics are marked with โญ.)
๐งฑ PART 1: Interface Segregation Principle (ISP)
1. ๐ค First, what is an interface?
๐ An interface is a contract that specifies what operations an object should provide.
Think about a TV remote ๐บ. It has buttons for power, volume, channel and mute. You don't need to know how the TV performs those operations internally. You only need to know which operations are available. That's the basic idea of an interface.
In Python, we commonly represent interfaces using:
- Abstract Base Classes (ABC)
-
Abstract methods using
@abstractmethod -
Protocols using
typing.Protocol
We'll use the ABC approach first.
2. ๐ What is the Interface Segregation Principle?
๐ Clients should not be forced to depend on interfaces they do not use.
In simple English: imagine an interface with 10 methods, but a particular class only needs 3. Should you force that class to implement all 10? NO! Instead, divide the large interface into smaller, specific interfaces, so each class implements only the methods relevant to it. That's ISP.
3. ๐จ The problem, with a real-world example
An employee management system has three kinds of employees: Developer, Manager, Intern. Suppose one common Employee interface contains:
work()attend_meeting()approve_leave()write_code()
But look at what each role really does:
| Employee | Work | Meeting | Approve leave | Write code |
|---|---|---|---|---|
| Developer | โ | โ | โ | โ |
| Manager | โ | โ | โ | Not necessarily |
| Intern | โ | โ | โ | โ |
A developer or an intern shouldn't be forced to implement approve_leave(). The interface is too large and mixes unrelated responsibilities. That's called a fat interface. ๐ท
4. โ Without ISP: bad implementation
from abc import ABC, abstractmethod
class Employee(ABC):
@abstractmethod
def work(self):
pass
@abstractmethod
def attend_meeting(self):
pass
@abstractmethod
def approve_leave(self):
pass
@abstractmethod
def write_code(self):
pass
Employee is an abstract class that forces every child to implement all four methods. Now a developer:
class Developer(Employee):
def work(self):
print("Developer is working")
def attend_meeting(self):
print("Developer is attending meeting")
def approve_leave(self):
raise NotImplementedError
def write_code(self):
print("Developer is writing code")
See approve_leave()? A developer doesn't approve leave! We're implementing a method the developer doesn't need, which is exactly the problem ISP prevents.
Problems with this design:
- Unnecessary methods are forced on classes
- Classes contain meaningless implementations
- Changes to the large interface can affect unrelated classes
- Code becomes harder to maintain
- It can cause runtime errors when unsupported methods are called
5. โ With ISP: correct implementation
Instead of one big interface, split it into smaller ones:
Before ISP: Employee โ work() ยท attend_meeting() ยท approve_leave() ยท write_code()
(ONE LARGE INTERFACE)
After ISP: Workable โ work()
MeetingParticipant โ attend_meeting()
LeaveApprover โ approve_leave()
CodeWriter โ write_code()
(small, focused interfaces)
from abc import ABC, abstractmethod
class Workable(ABC):
@abstractmethod
def work(self):
pass
class MeetingParticipant(ABC):
@abstractmethod
def attend_meeting(self):
pass
class LeaveApprover(ABC):
@abstractmethod
def approve_leave(self):
pass
class CodeWriter(ABC):
@abstractmethod
def write_code(self):
pass
Now each interface has one specific responsibility. Let's build the classes:
class Developer(Workable, MeetingParticipant, CodeWriter):
def work(self):
print("Developer is working")
def attend_meeting(self):
print("Developer is attending meeting")
def write_code(self):
print("Developer is writing code")
class Manager(Workable, MeetingParticipant, LeaveApprover):
def work(self):
print("Manager is managing the team")
def attend_meeting(self):
print("Manager is attending meeting")
def approve_leave(self):
print("Manager approved leave")
class Intern(Workable, MeetingParticipant, CodeWriter):
def work(self):
print("Intern is learning and working")
def attend_meeting(self):
print("Intern is attending meeting")
def write_code(self):
print("Intern is writing code")
Each class implements only the relevant interfaces. Nobody is forced to implement unnecessary methods. That's the Interface Segregation Principle! ๐ฏ
6. ๐ท Fat interface vs thin interface
| Fat interface | Thin interface |
|---|---|
| Contains too many methods | Contains fewer focused methods |
| Includes unrelated operations | Includes related operations |
| Forces unnecessary implementations | Classes implement what they need |
| Difficult to maintain | Easier to maintain |
๐ง Fat interface = too many unrelated responsibilities. Thin interface = a small, focused contract.
7. ๐ณ Another example: a payment system
E-commerce payment methods: credit card, UPI, cash on delivery. Suppose you create one interface:
class PaymentMethod(ABC):
@abstractmethod
def pay(self):
pass
@abstractmethod
def refund(self):
pass
@abstractmethod
def scan_qr(self):
pass
The problem:
- Credit cards can pay and refund
- UPI can pay, refund and scan QR codes
- Cash on delivery might not support online refunds or QR scanning
One interface forces every payment method to support everything. Split it instead:
class Payable(ABC):
@abstractmethod
def pay(self):
pass
class Refundable(ABC):
@abstractmethod
def refund(self):
pass
class QRScannable(ABC):
@abstractmethod
def scan_qr(self):
pass
Now payment implementations choose the capabilities they actually support. This is useful in payment gateways, notification systems, file processing and authentication systems.
8. โก ISP in real full-stack development
In a social-feed backend you might have normal users, admins and content moderators, with operations like create post, like post, comment, delete any user's post, block users and manage reports. Would you give every user every operation? Of course not! A normal user creates posts and comments, while an admin can also delete inappropriate posts and manage users. Separate these capabilities into focused interfaces:
class PostCreator(ABC):
@abstractmethod
def create_post(self):
pass
class PostModerator(ABC):
@abstractmethod
def delete_post(self):
pass
class UserManager(ABC):
@abstractmethod
def block_user(self):
pass
Different classes implement different capabilities, which makes it easier to extend when new roles and permissions arrive.
โ ๏ธ ISP is about designing small, cohesive interfaces. It does not mean every single method needs its own interface. Group methods that naturally belong together.
9. โ Advantages of ISP
- ๐งน Less unnecessary code: classes don't implement methods they don't need
- ๐ Loose coupling: classes depend on fewer operations
- ๐ ๏ธ Easy maintenance: changes to one interface are less likely to affect unrelated classes
- ๐ Better readability: each interface has a clear purpose
- ๐งช Better testing: test smaller components independently
- ๐ Better scalability: new capabilities don't require changing a huge interface
10. ๐ต๏ธ When should you use ISP?
Use it when:
- An interface has too many unrelated methods
- Classes implement methods they don't actually support
- You see
NotImplementedErrorin classes just because the parent interface forces it - Different clients need different subsets of operations
โ ๏ธ ISP doesn't mean creating hundreds of tiny interfaces without a reason. Over-segregation also makes code hard to understand.
๐ PART 2: Dependency Inversion Principle (DIP)
๐๏ธ One of the most important SOLID principles for understanding professional backend architecture.
To understand DIP we need three concepts first: dependency, abstraction vs concretion, and dependency inversion.
1. ๐ What is a dependency?
class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self):
self.engine = Engine()
def start_car(self):
self.engine.start()
The car needs an engine to operate, so Car depends on Engine.
๐ When one class needs another class to perform its work, the first class has a dependency on the second.
Car class (needs an engine to work)
โ depends on
โผ
Engine class (provides start() functionality)
But what if you want an electric or hybrid engine? Car directly creates a specific Engine, which makes it tightly coupled to that implementation.
2. ๐ชข What is tight coupling?
Coupling describes how strongly two software components depend on each other.
- Tight coupling: one class is strongly dependent on the internal implementation of another
- Loose coupling: one class depends on a stable contract rather than a particular implementation
class DieselEngine:
def start(self):
print("Diesel engine started")
class Car:
def __init__(self):
self.engine = DieselEngine()
def start_car(self):
self.engine.start()
The problem line:
self.engine = DieselEngine()
Car directly creates a DieselEngine. Tomorrow you need an electric one:
class ElectricEngine:
def start(self):
print("Electric engine started")
...and now you must modify the Car class. As the app grows with many implementations, changing them becomes hard when business logic directly creates them. That's tight coupling. ๐ฉ
3. ๐ญ What is abstraction?
๐ Abstraction means hiding implementation details and exposing only the essential functionality.
When you transfer money in a banking app ๐ฆ, you enter the amount, enter the receiver and click Transfer. Do you need to know the database queries, server communication, transaction validation and network operations? No! You interact with one simple operation, and the complicated internals are hidden. That's abstraction.
โ๏ธ Abstraction vs implementation
| Abstraction | Implementation |
|---|---|
| What should happen | How it happens |
| Defines a contract | Provides actual logic |
| Focuses on functionality | Focuses on internal details |
| Example: Send notification | Example: Send using email |
In Python:
from abc import ABC, abstractmethod
class NotificationService(ABC):
@abstractmethod
def send(self, message):
pass
This is an abstraction. It says "any notification service must provide a send() method", but not how to send. Now implement it in different ways:
class EmailNotification(NotificationService):
def send(self, message):
print("Sending email:", message)
class SMSNotification(NotificationService):
def send(self, message):
print("Sending SMS:", message)
class PushNotification(NotificationService):
def send(self, message):
print("Sending push notification:", message)
These are concrete implementations: each follows the same contract but performs the operation differently.
NotificationService โ abstraction ยท send(message)
โฑ โ โฒ
Email SMS Push โ concrete implementations
๐งฑ What is a concretion?
A concretion is an actual class that provides the implementation of an abstraction.
-
NotificationServiceโ abstraction -
EmailNotification,SMSNotification,PushNotificationโ concrete implementations
๐ง Abstraction defines WHAT. Concretion defines HOW.
4. ๐ What exactly is the Dependency Inversion Principle?
The official definition:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
Translated: your application has two kinds of layers.
- High-level module: contains important business logic (e.g. an order service that processes customer orders)
- Low-level module: performs technical operations (sending emails, saving to a database, uploading images, sending SMS)
The high-level business logic should not be tightly connected to one specific low-level implementation. Instead, both depend on an abstraction. That's DIP!
5. ๐๏ธ High-level vs low-level modules
A customer places an order. What should happen?
- Validate the order
- Calculate the total
- Save the order
- Send a confirmation email
| Component | Type |
|---|---|
OrderService |
๐ High-level module |
EmailService |
๐ง Low-level module |
DatabaseRepository |
๐ง Low-level module |
PaymentGateway |
๐ง Low-level module |
OrderService is high-level because it contains the important business workflow. Email and database services are low-level because they perform specific technical operations used by that workflow.
6. โ Without DIP: bad implementation
class EmailService:
def send_email(self, message):
print("Email sent:", message)
class OrderService:
def __init__(self):
self.email_service = EmailService()
def place_order(self):
print("Order placed successfully")
self.email_service.send_email(
"Your order has been confirmed"
)
The key line:
self.email_service = EmailService()
OrderService directly creates an EmailService, so:
- It knows about
EmailService - It's tightly coupled to
EmailService - It can't easily switch to SMS or push notifications
If your manager says "we now want order confirmations through SMS instead of email", you must change OrderService. That's undesirable, because the business workflow shouldn't change just because the notification technology changes. This violates DIP. ๐จ
7. โ With DIP: correct implementation
Step 1: create an abstraction
from abc import ABC, abstractmethod
class NotificationService(ABC):
@abstractmethod
def send(self, message):
pass
Step 2: create concrete implementations
class EmailService(NotificationService):
def send(self, message):
print("Email sent:", message)
class SMSService(NotificationService):
def send(self, message):
print("SMS sent:", message)
Step 3: modify the high-level module
class OrderService:
def __init__(self, notification_service):
self.notification_service = notification_service
def place_order(self):
print("Order placed successfully")
self.notification_service.send(
"Your order has been confirmed"
)
Previously we created the email service inside OrderService. Now we receive the notification service from outside. A major difference! ๐
Step 4: inject the implementation
email_service = EmailService()
order_service = OrderService(email_service)
order_service.place_order()
Order placed successfully
Email sent: Your order has been confirmed
Want SMS instead?
sms_service = SMSService()
order_service = OrderService(sms_service)
order_service.place_order()
Order placed successfully
SMS sent: Your order has been confirmed
Did we modify OrderService? NO! We just passed a different implementation. That's the power of DIP. ๐ช
8. ๐ Exactly what changed?
โ Without DIP
OrderService (business logic) โโโถ EmailService (specific implementation)
tightly coupled
โ
With DIP
OrderService (high-level) โโโถ NotificationService (abstraction / contract) โโโ EmailService
โโโ SMSService
loosely coupled architecture
Previously: OrderService โ EmailService
After applying DIP: OrderService โ NotificationService โ EmailService
Both the high-level and low-level components depend on the abstraction. The dependency direction has been inverted. ๐
9. ๐ What is Dependency Injection (DI)?
You may have heard about Dependency Injection in FastAPI. DIP and DI are related, but they are NOT the same.
๐ Dependency Injection means providing an object or dependency to a class from outside instead of making that class create it internally.
Without DI:
class OrderService:
def __init__(self):
self.payment = PaymentService()
With DI:
class OrderService:
def __init__(self, payment):
self.payment = payment
โ๏ธ DIP vs DI
| DIP | DI |
|---|---|
| A design principle | A technique / pattern |
| Focuses on abstractions | Focuses on providing dependencies |
| Reduces tight coupling | Helps achieve loose coupling |
| Architectural guideline | Practical implementation technique |
- DIP tells you how your architecture should be designed
- DI gives you a way to supply the required objects
You can use DI to implement DIP. But simply injecting a concrete class doesn't automatically guarantee DIP. The dependency should be designed around an appropriate abstraction.
10. โก How does this work in FastAPI?
FastAPI has a dependency injection system called Depends():
from fastapi import Depends, FastAPI
app = FastAPI()
class UserService:
def get_user(self, user_id: int):
return {
"id": user_id,
"name": "Deepika",
}
def get_user_service():
return UserService()
@app.get("/users/{user_id}")
def get_user(
user_id: int,
service: UserService = Depends(get_user_service),
):
return service.get_user(user_id)
The flow:
- A client sends a GET request
- FastAPI sees
Depends(get_user_service) - FastAPI calls the dependency provider
- The provider creates and returns a
UserServiceobject - FastAPI passes that object to the endpoint
- The endpoint calls
service.get_user()
The endpoint doesn't create the service itself. That's dependency injection.
HTTP Request GET /users/10
โผ
FastAPI Router โ endpoint receives the request
โผ
Depends() โ requests the dependency
โผ
get_user_service() โ provides a UserService object
โผ
Endpoint uses the service โ service.get_user(user_id)
๐
Depends()is the DI mechanism. To achieve DIP, you should also design your service and repository dependencies around abstractions when appropriate.
11. ๐๏ธ A more realistic FastAPI example: the repository pattern
A very important example for backend architecture. Your layers: API/router โ service โ repository โ database. The service needs user data, but it shouldn't need to know which database technology is used. Today it's PostgreSQL. Tomorrow it might be another database, or an in-memory repository for testing.
Step 1: define the repository abstraction
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
def get_user(self, user_id: int):
pass
Step 2: create a concrete implementation
class PostgreSQLUserRepository(UserRepository):
def get_user(self, user_id: int):
print("Fetching user from PostgreSQL")
return {
"id": user_id,
"name": "Deepika",
}
Step 3: create the high-level service
class UserService:
def __init__(self, repository: UserRepository):
self.repository = repository
def get_user_details(self, user_id: int):
user = self.repository.get_user(user_id)
return user
Notice repository: UserRepository. The service depends on the abstraction, not directly on PostgreSQLUserRepository.
Step 4: inject the implementation
repository = PostgreSQLUserRepository()
user_service = UserService(repository)
result = user_service.get_user_details(10)
print(result)
Fetching user from PostgreSQL
{'id': 10, 'name': 'Deepika'}
Now replace PostgreSQL with an in-memory implementation:
class InMemoryUserRepository(UserRepository):
def get_user(self, user_id: int):
return {
"id": user_id,
"name": "Test User",
}
repository = InMemoryUserRepository()
user_service = UserService(repository)
print(user_service.get_user_details(10))
You changed the repository implementation without changing UserService. That's DIP in action. ๐ฏ
Real-world connection to a social-feed backend:
-
PostServiceโ post-related business logic -
PostRepositoryโ an interface for post data access -
PostgresPostRepositoryโ the actual PostgreSQL implementation
The service shouldn't need to know the SQL queries the repository uses.
๐ This is a simplified architectural example. A real SQLAlchemy implementation may use
AsyncSession, async repository methods and FastAPI dependencies.
12. ๐งช Why is DIP great for testing?
When testing UserService, it normally connects to PostgreSQL. But what if your database is unavailable, you don't want to create real records, you want predictable test data, or you want to test business logic independently? Inject a fake repository:
class FakeUserRepository(UserRepository):
def get_user(self, user_id: int):
return {
"id": user_id,
"name": "Testing User",
}
fake_repository = FakeUserRepository()
service = UserService(fake_repository)
result = service.get_user_details(10)
assert result["name"] == "Testing User"
We tested UserService without connecting to PostgreSQL! That's possible because the service depends on an abstraction, so we can supply a different implementation. A hugely useful technique for unit testing.
13. ๐ง DIP vs DI vs IoC
These three terms confuse everyone. Let's separate them:
| Concept | Meaning |
|---|---|
| DIP | High-level and low-level modules depend on abstractions |
| DI | Dependencies are provided from outside |
| IoC | Control over object creation or execution is handed to another component or framework |
Inversion of Control (IoC) is the broader concept. In a traditional program you create and manage all objects yourself. With FastAPI, the framework manages dependency resolution when it processes a request. That's IoC. DI is one way to implement IoC, and DIP is an architectural principle that keeps components independent of concrete implementations.
14. โ Advantages of DIP
- ๐ Loose coupling: business logic isn't tied to specific technologies
- ๐ Easy replacement: swap PostgreSQL for another repository implementation
- ๐งช Easy testing: inject fake or mock implementations
- ๐ ๏ธ Maintainability: technical changes don't necessarily affect business logic
- ๐ Scalability: add new implementations without rewriting business logic
- ๐ Flexibility: switch email providers, payment gateways, storage services and more
15. ๐ต๏ธ When should you use DIP?
Use it when:
- Business logic directly creates database or external service objects
- You want to support multiple implementations
- You want to test business logic independently
- Your application has multiple architectural layers
- You want to avoid tightly coupling services to specific technologies
โ ๏ธ Don't create abstractions for every class unnecessarily. A simple app with no meaningful variation may not need a separate interface for every dependency.
โ ๏ธ PART 3: Common Mistakes and Important Differences
1. โ๏ธ ISP vs DIP
| ISP | DIP |
|---|---|
| Interface Segregation Principle | Dependency Inversion Principle |
| Focuses on interface size and design | Focuses on dependency direction |
| Avoids unnecessary methods | Avoids dependence on concrete implementations |
| Uses small, focused interfaces | Uses abstractions between modules |
| Example: separate payment capabilities | Example: PaymentService depends on a PaymentGateway abstraction |
๐ง ISP: "Don't force me to implement methods I don't need." DIP: "Don't force me to depend on a specific implementation."
2. ๐ Abstraction vs concretion: quick revision
| Example | Abstraction | Concrete implementation |
|---|---|---|
| Notification | NotificationService |
EmailService |
| Database | UserRepository |
PostgreSQLUserRepository |
| Payment | PaymentGateway |
RazorpayGateway |
| Storage | FileStorage |
S3Storage |
| Authentication | AuthProvider |
GoogleAuthProvider |
๐ These are examples of possible designs. An abstraction doesn't have to be a Python ABC. It can also be a
Protocolor another suitable contract.
3. โ Common mistakes to avoid
Mistake 1: thinking DIP and DI are the same. They aren't. DIP is a principle; DI is a technique.
Mistake 2: thinking every interface must have only one method. No. Interfaces should group related operations. They should just avoid forcing unrelated operations on clients.
Mistake 3: thinking abstraction means hiding all code. No. Abstraction means exposing essential functionality while hiding unnecessary implementation details.
Mistake 4: thinking DIP means never depending on concrete classes. Not exactly. Concrete implementations must exist and be instantiated somewhere. The important thing is that high-level business logic shouldn't be tightly coupled to them.
Mistake 5: thinking FastAPI's Depends() automatically guarantees DIP. No. It provides dependency injection, but your architecture must still be designed appropriately.
๐ค PART 4: Interview Preparation
Q1. What is the Interface Segregation Principle?
ISP states that clients should not be forced to depend on interfaces they do not use. We divide large interfaces into smaller, focused ones so classes only implement relevant methods.
Q2. What is a fat interface?
An interface containing too many methods, often including unrelated operations that force implementing classes to provide unnecessary functionality.
Q3. What is the Dependency Inversion Principle?
DIP states that high-level modules should not depend directly on low-level modules. Both should depend on abstractions. This reduces coupling and makes applications easier to maintain and test.
Q4. What's the difference between abstraction and implementation?
Abstraction defines what functionality should be provided, while implementation defines how it's performed.
Q5. What is tight coupling?
When one component strongly depends on another component's specific implementation, making changes and replacements difficult.
Q6. What is Dependency Injection?
A technique in which a class receives its required dependencies from an external source instead of creating them internally.
Q7. How does FastAPI support Dependency Injection?
Through Depends(), which lets dependencies such as services, database sessions and authentication logic be provided to endpoints.
Q8. How does DIP improve testing?
Since business logic depends on abstractions, we can inject mock or fake implementations instead of connecting to actual external services or databases.
๐งช PART 5: Test Your Understanding!
Try these before peeking at the answers! โค๏ธ
-
A class is forced to implement a method that it never uses. Which principle is violated?
- A) DIP
- B) ISP
- C) SRP
- D) OCP
-
An
OrderServicedirectly createsEmailServiceinside its constructor. What's the main design problem?- A) Tight coupling
- B) Encapsulation
- C) Polymorphism
- D) Normalization
-
What should high-level and low-level modules depend on, according to DIP?
- A) Concrete classes
- B) Database tables
- C) Abstractions
- D) Global variables
-
You pass a fake repository into
UserServicefor testing. What technique are you using?- A) Inheritance only
- B) Dependency Injection
- C) Normalization
- D) Overloading
-
Which is a concrete implementation?
- A)
NotificationServiceabstraction - B)
UserRepositorycontract - C)
EmailService - D) Payment interface
- A)
๐ Click to reveal answers
EmailService is an actual class that provides the implementation
๐ PART 6: Final Revision, All Five SOLID Principles
| Letter | Principle | Idea | Example |
|---|---|---|---|
| S | Single Responsibility | One class should have one reason to change | Separate email sending from order processing |
| O | Open/Closed | Open for extension, closed for modification | Add a new payment method without modifying existing payment logic |
| L | Liskov Substitution | Child classes should be replaceable wherever their parent is expected, without breaking correct behavior | Any valid subtype of a payment processor satisfies the payment contract |
| I | Interface Segregation | Don't force classes to implement methods they don't need | Separate Workable, LeaveApprover and CodeWriter interfaces |
| D | Dependency Inversion | Depend on abstractions, not concrete implementations |
UserService depends on UserRepository, not PostgreSQLUserRepository
|
๐งญ Five Questions to Ask When Designing Your Backend
- S: Does this class have too many responsibilities?
- O: Can I add new functionality without unnecessarily changing existing code?
- L: Can I replace a child implementation without breaking the expected behavior?
- I: Am I forcing a class to implement unnecessary methods?
- D: Am I depending on a concrete implementation when I should depend on an abstraction?
If you can answer these while designing your FastAPI services, repositories and database layers, you're starting to think like a software engineer, not just someone who writes code. ๐ง
If this post helped you, drop a โค๏ธ or ๐ฆ and share it with a friend learning software design. Happy coding! ๐ฉโ๐ปโจ
Top comments (0)