🧩 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
Related posts

Serving Groot-19B-A3B at 256K on one RTX 5090: the memory budget and the thinking trap
What Is Ethical Hacking? A Beginner's Guide
A Short Text GIF for a Weekend Sale: Wording, Effect and Size
Seven Studio Pages, No Limits of Their Own: The Numbers Are in the Tools, Not the Menu

Making AI-Generated Websites Match Your Brand: Structure, Style, and Local Edits
