0
0
LLDsystem_design~7 mins

Dependency Inversion Principle in LLD - System Design Guide

Choose your learning style9 modes available
Problem Statement
When high-level modules depend directly on low-level modules, changes in low-level details force changes in high-level logic. This tight coupling makes the system fragile and hard to maintain or extend.
Solution
This principle suggests that both high-level and low-level modules should depend on abstractions, not on concrete implementations. By introducing interfaces or abstract classes, the system decouples the high-level logic from low-level details, allowing independent changes and easier testing.
Architecture
High-Level
Module
Abstraction

This diagram shows both high-level and low-level modules depending on a shared abstraction, which decouples their implementations.

Trade-offs
✓ Pros
Improves code maintainability by reducing tight coupling between modules.
Enables easier unit testing by allowing mock implementations of abstractions.
Supports flexible system extension without modifying existing high-level logic.
✗ Cons
Introduces additional abstraction layers, which can increase initial complexity.
May require more upfront design effort to define proper interfaces.
Can lead to over-engineering if applied unnecessarily in simple systems.
Use when your system has complex business logic that depends on changing low-level details or external services, especially in medium to large codebases with multiple developers.
Avoid when building small, simple applications where abstractions add unnecessary complexity and the codebase is unlikely to change frequently.
Real World Examples
Amazon
Amazon uses dependency inversion to decouple their order processing logic from payment gateway implementations, allowing easy integration of new payment providers without changing core order logic.
Netflix
Netflix applies this principle to separate their streaming service logic from different storage backends, enabling smooth migration and testing.
Uber
Uber uses abstractions to isolate ride matching algorithms from data sources, allowing independent evolution of matching logic and data storage.
Code Example
The before code shows UserService directly creating a MySQLDatabase instance, causing tight coupling. The after code introduces a Database abstraction that UserService depends on, allowing any database implementation to be injected, improving flexibility and testability.
LLD
### Before applying Dependency Inversion Principle (tightly coupled)
class MySQLDatabase:
    def connect(self):
        print("Connecting to MySQL database")

class UserService:
    def __init__(self):
        self.db = MySQLDatabase()  # Direct dependency

    def get_user(self, user_id):
        self.db.connect()
        print(f"Fetching user {user_id}")


### After applying Dependency Inversion Principle (depends on abstraction)
from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def connect(self):
        pass

class MySQLDatabase(Database):
    def connect(self):
        print("Connecting to MySQL database")

class UserService:
    def __init__(self, db: Database):
        self.db = db  # Depends on abstraction

    def get_user(self, user_id):
        self.db.connect()
        print(f"Fetching user {user_id}")


# Usage
mysql_db = MySQLDatabase()
service = UserService(mysql_db)
service.get_user(42)
OutputSuccess
Alternatives
Service Locator
Service Locator centralizes dependency resolution in a registry, whereas Dependency Inversion uses explicit abstractions passed to modules.
Use when: Choose Service Locator when you want to centralize configuration but accept some hidden dependencies.
Factory Pattern
Factory creates objects without exposing instantiation logic, while Dependency Inversion focuses on depending on abstractions rather than concrete classes.
Use when: Choose Factory when object creation logic is complex or varies, but still want to follow Dependency Inversion for dependencies.
Summary
Dependency Inversion Principle reduces tight coupling by making high-level modules depend on abstractions.
It improves maintainability, testability, and flexibility by decoupling implementation details.
Applying it requires thoughtful design of interfaces and careful use of dependency injection.