DEV Community

Deepika Pusala
Deepika Pusala

Posted on

SOLID Principles Deep Dive (I & D): Interface Segregation and Dependency Inversion Explained ๐Ÿงฉ

๐Ÿงฉ 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:

  1. Abstract Base Classes (ABC)
  2. Abstract methods using @abstractmethod
  3. 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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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:

  1. Unnecessary methods are forced on classes
  2. Classes contain meaningless implementations
  3. Changes to the large interface can affect unrelated classes
  4. Code becomes harder to maintain
  5. 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)
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. ๐Ÿงน Less unnecessary code: classes don't implement methods they don't need
  2. ๐Ÿ”— Loose coupling: classes depend on fewer operations
  3. ๐Ÿ› ๏ธ Easy maintenance: changes to one interface are less likely to affect unrelated classes
  4. ๐Ÿ“– Better readability: each interface has a clear purpose
  5. ๐Ÿงช Better testing: test smaller components independently
  6. ๐Ÿ“ˆ 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 NotImplementedError in 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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

The problem line:

self.engine = DieselEngine()
Enter fullscreen mode Exit fullscreen mode

Car directly creates a DieselEngine. Tomorrow you need an electric one:

class ElectricEngine:
    def start(self):
        print("Electric engine started")
Enter fullscreen mode Exit fullscreen mode

...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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

These are concrete implementations: each follows the same contract but performs the operation differently.

              NotificationService      โ† abstraction ยท send(message)
             โ•ฑ         โ”‚          โ•ฒ
          Email       SMS         Push     โ† concrete implementations
Enter fullscreen mode Exit fullscreen mode

๐Ÿงฑ 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:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. 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?

  1. Validate the order
  2. Calculate the total
  3. Save the order
  4. 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"
        )
Enter fullscreen mode Exit fullscreen mode

The key line:

self.email_service = EmailService()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
        )
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode
Order placed successfully
Email sent: Your order has been confirmed
Enter fullscreen mode Exit fullscreen mode

Want SMS instead?

sms_service = SMSService()
order_service = OrderService(sms_service)
order_service.place_order()
Enter fullscreen mode Exit fullscreen mode
Order placed successfully
SMS sent: Your order has been confirmed
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

With DI:

class OrderService:
    def __init__(self, payment):
        self.payment = payment
Enter fullscreen mode Exit fullscreen mode

โš–๏ธ 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)
Enter fullscreen mode Exit fullscreen mode

The flow:

  1. A client sends a GET request
  2. FastAPI sees Depends(get_user_service)
  3. FastAPI calls the dependency provider
  4. The provider creates and returns a UserService object
  5. FastAPI passes that object to the endpoint
  6. 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)
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ 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
Enter fullscreen mode Exit fullscreen mode

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",
        }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
Fetching user from PostgreSQL
{'id': 10, 'name': 'Deepika'}
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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

  1. ๐Ÿ”— Loose coupling: business logic isn't tied to specific technologies
  2. ๐Ÿ”„ Easy replacement: swap PostgreSQL for another repository implementation
  3. ๐Ÿงช Easy testing: inject fake or mock implementations
  4. ๐Ÿ› ๏ธ Maintainability: technical changes don't necessarily affect business logic
  5. ๐Ÿ“ˆ Scalability: add new implementations without rewriting business logic
  6. ๐Ÿ”€ 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 Protocol or 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! โค๏ธ

  1. A class is forced to implement a method that it never uses. Which principle is violated?

    • A) DIP
    • B) ISP
    • C) SRP
    • D) OCP
  2. An OrderService directly creates EmailService inside its constructor. What's the main design problem?

    • A) Tight coupling
    • B) Encapsulation
    • C) Polymorphism
    • D) Normalization
  3. What should high-level and low-level modules depend on, according to DIP?

    • A) Concrete classes
    • B) Database tables
    • C) Abstractions
    • D) Global variables
  4. You pass a fake repository into UserService for testing. What technique are you using?

    • A) Inheritance only
    • B) Dependency Injection
    • C) Normalization
    • D) Overloading
  5. Which is a concrete implementation?

    • A) NotificationService abstraction
    • B) UserRepository contract
    • C) EmailService
    • D) Payment interface

๐Ÿ‘€ Click to reveal answers
  1. B: ISP. The class is being forced to depend on a method it doesn't use
  2. A: Tight coupling. The service is bound to one specific implementation
  3. C: Abstractions. Both layers should depend on contracts
  4. B: Dependency Injection. The dependency is supplied from outside
  5. C: 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

  1. S: Does this class have too many responsibilities?
  2. O: Can I add new functionality without unnecessarily changing existing code?
  3. L: Can I replace a child implementation without breaking the expected behavior?
  4. I: Am I forcing a class to implement unnecessary methods?
  5. 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)