Dependency Inversion Principle: Decoupling Your Code
High-level modules shouldn't depend on low-level modules. Both should depend on abstractions. We explain DIP with a simple Java example.
Abstract AlgorithmsTLDR: The Dependency Inversion Principle (DIP) states that high-level business logic should depend on abstractions (interfaces), not on concrete implementations (MySQL, SendGrid, etc.). This lets you swap a database or email provider without touching your business rules.
๐ The Tightly Coupled Trap: When Changing a Database Breaks Business Logic
You're building an order service. The first version looks like this:
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # direct dependency
def place_order(self, order):
self.db.save(order)
EmailClient().send_confirmation(order)
Six months later, you migrate to PostgreSQL. You search for every place MySQLDatabase() is used โ it's woven through your order logic, your billing logic, your reporting. Changing the database means touching business code.
This is a dependency inversion violation: a high-level module (OrderService) depends directly on low-level modules (MySQLDatabase, EmailClient).
โ๏ธ The Principle: Abstractions Own the Contract
The Dependency Inversion Principle has two parts:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
graph LR
subgraph Before DIP
Biz1[OrderService] --> MySQL[MySQLDatabase]
Biz1 --> Email1[SendGridClient]
end
subgraph After DIP
Biz2[OrderService] --> IRepo[IOrderRepository\nabstraction]
Biz2 --> IEmail[IEmailService\nabstraction]
IRepo --> MySQL2[MySQLRepository]
IRepo --> PG[PostgresRepository]
IEmail --> Email2[SendGridEmailService]
IEmail --> Mock[MockEmailService]
end
๐ข The Interface as the Contract: Python and Java Examples
Python (using ABC):
from abc import ABC, abstractmethod
class IOrderRepository(ABC):
@abstractmethod
def save(self, order) -> None: ...
@abstractmethod
def find_by_id(self, order_id: str): ...
class MySQLOrderRepository(IOrderRepository):
def save(self, order):
# MySQL-specific implementation
...
class PostgresOrderRepository(IOrderRepository):
def save(self, order):
# Postgres-specific implementation
...
class OrderService:
def __init__(self, repo: IOrderRepository): # depends on interface
self.repo = repo
def place_order(self, order):
self.repo.save(order)
Switch to Postgres: OrderService(PostgresOrderRepository()). No changes to OrderService.
Java (using interface):
public interface IOrderRepository {
void save(Order order);
Order findById(String id);
}
public class OrderService {
private final IOrderRepository repo; // depends on interface
public OrderService(IOrderRepository repo) {
this.repo = repo;
}
public void placeOrder(Order order) {
repo.save(order);
}
}
๐ง Dependency Injection: How DIP Is Wired at Runtime
DIP says what to depend on (abstractions). Dependency Injection (DI) says how to supply the concrete implementation at runtime.
| Style | Example |
| Constructor injection | OrderService(MySQLOrderRepository()) |
| DI container (Spring) | @Autowired IOrderRepository repo; |
| Factory function | make_order_service(db="mysql") |
Constructor injection is the simplest and most testable option for most cases.
Testing benefit: With DIP, unit tests can inject a mock:
class FakeOrderRepository(IOrderRepository):
def __init__(self):
self.saved = []
def save(self, order):
self.saved.append(order)
def test_place_order():
repo = FakeOrderRepository()
svc = OrderService(repo)
svc.place_order(Order(id="1"))
assert len(repo.saved) == 1
No database needed to test business logic.
โ๏ธ When DIP Adds Complexity Without Value
DIP is not always the right answer:
| Situation | Recommendation |
| One concrete implementation, no plans to swap | Skip the interface; add it later if needed |
| Utility/helper functions with no side effects | Interfaces add ceremony with no benefit |
| Scripts and one-off tools | Direct imports are fine |
| Library-internal code | Only apply at public boundaries |
A sign that DIP is applied incorrectly: your project has 40 interfaces, all of which have exactly one implementation. YAGNI โ don't abstract what you don't need.
๐ Key Takeaways
- DIP: high-level modules + low-level modules should both depend on abstractions, not on each other.
- The abstraction (interface) is the stable contract; the concrete implementation is the swappable detail.
- The main win: you can swap databases, email providers, or payment gateways without touching business logic.
- Dependency Injection is the mechanism that supplies the concrete implementation at runtime.
- Don't over-apply DIP: only abstract at decision boundaries where swap is realistic.
๐งฉ Test Your Understanding
PaymentServiceclasses directly instantiateStripeClient. Which DIP rule does this violate?- Why does DIP make unit testing easier?
- You have
IEmailServicewith two implementations (SendGrid,SES). Is DIP being applied well? What would make it questionable? - What is the difference between the Dependency Inversion Principle and Dependency Injection?
๐ Related Posts

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
SFT for LLMs: A Practical Guide to Supervised Fine-Tuning
TLDR: Supervised fine-tuning (SFT) is the stage where a pretrained model learns task-specific response behavior from curated input-output examples. It is usually the first alignment step after pretraining and often the foundation for later RLHF. Good...
RLHF in Practice: From Human Preferences to Better LLM Policies
TLDR: Reinforcement Learning from Human Feedback (RLHF) helps align language models with human preferences after pretraining and SFT. The typical pipeline is: collect preference comparisons, train a reward model, then optimize a policy (often with KL...
PEFT, LoRA, and QLoRA: A Practical Guide to Efficient LLM Fine-Tuning
TLDR: Full fine-tuning updates every model weight, which is expensive in memory, compute, and storage. PEFT methods update only a small trainable slice. LoRA learns low-rank adapters on top of frozen base weights. QLoRA pushes efficiency further by q...
LLM Model Naming Conventions: How to Read Names and Why They Matter
TLDR: LLM names encode practical decisions: model family, size, training stage, context window, format, and quantization level. If you can decode naming conventions, you can avoid costly deployment mistakes and choose the right checkpoint faster. ๏ฟฝ...
