On this page
Description
The Dependency Inversion Principle says business rules shouldn't depend on databases, email servers, clocks or other details - both should depend on an abstraction that the business code owns. It's what makes core logic testable in milliseconds and lets infrastructure change without touching it. This skill covers the principle, dependency injection as the usual way to apply it, and how to avoid wrapping everything in interfaces.
Here's the kind of code it's about:
Java
class OrderService {
private final MySqlOrderDao dao = new MySqlOrderDao(); // concrete database
private final SmtpMailer mailer = new SmtpMailer("smtp.acme.com"); // concrete email server
void placeOrder(Order order) {
dao.insert(order);
mailer.send(order.customerEmail(), "Thanks for your order!");
}
}
Python
class OrderService:
def __init__(self):
self._dao = MySqlOrderDao() # concrete database
self._mailer = SmtpMailer("smtp.acme.com") # concrete email server
def place_order(self, order: Order) -> None:
self._dao.insert(order)
self._mailer.send(order.customer_email, "Thanks for your order!")
The most important code in the system - the business rule "place an order" - is welded to MySQL and one SMTP server. You can't test it without a database and a mail server, and switching to Postgres or an email API means editing OrderService.
We'll go from simple to more technical:
- The principle: abstractions owned by the high-level code.
- Applying it with constructor injection, step by step.
- Where you'll see it: DI containers, JDBC and DB-API, ports and adapters.
- When an interface isn't worth it, and how abstractions leak.
The principle
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.
The high-level code declares what it needs as an interface it owns; the details implement it; something outside wires them together.
Java
interface OrderRepository { void save(Order order); }
interface Notifier { void notify(String to, String message); }
class OrderService {
private final OrderRepository orders;
private final Notifier notifier;
OrderService(OrderRepository orders, Notifier notifier) { // details are injected
this.orders = orders;
this.notifier = notifier;
}
void placeOrder(Order order) {
orders.save(order);
notifier.notify(order.customerEmail(), "Thanks for your order!");
}
}
// Production: new OrderService(new PostgresOrderRepository(ds), new EmailApiNotifier(key));
// Tests: new OrderService(new InMemoryOrderRepository(), new RecordingNotifier());
Python
from typing import Protocol
class OrderRepository(Protocol):
def save(self, order: Order) -> None: ...
class Notifier(Protocol):
def notify(self, to: str, message: str) -> None: ...
class OrderService:
def __init__(self, orders: OrderRepository, notifier: Notifier): # details are injected
self._orders = orders
self._notifier = notifier
def place_order(self, order: Order) -> None:
self._orders.save(order)
self._notifier.notify(order.customer_email, "Thanks for your order!")
# Production: OrderService(PostgresOrderRepository(pool), EmailApiNotifier(key))
# Tests: OrderService(InMemoryOrderRepository(), RecordingNotifier())
How to apply it, step by step
- Identify the policy (business rules) and the details (databases, HTTP clients, clocks, file systems).
- Let the policy define small interfaces describing what it needs - in its own terms.
- Make the details implement those interfaces.
- Pass the details in through the constructor (dependency injection), instead of creating them inside.
- Wire everything in one place at startup - by hand or with a framework like Spring.
Where you'll see it
- Spring and other DI containers exist to do step 5 for you: constructor parameters typed as interfaces, implementations chosen by configuration. Python projects mostly wire by hand in one startup function (FastAPI's
Dependsis the web-framework version). - JDBC: your code talks to
java.sql.Connection; each database vendor ships a driver that implements it. (Python's equivalent is the DB-API, PEP 249:psycopg,sqlite3andmysqlclientall offer the sameconnect(),cursor()andexecute().) - Hexagonal / ports-and-adapters architecture: the domain defines ports (interfaces), infrastructure provides adapters.
Which tool for which job?
| The dependency | Do this |
|---|---|
| A database, message broker, HTTP API or file system | an interface (port) owned by the business code, implemented by an adapter |
| The current time | inject a Clock (Java) or a now callable (Python) |
| Random numbers or ids | inject a generator so tests can fix them |
| Stable, side-effect-free helpers and value objects | use them directly - no interface needed |
| Wiring it all together | one composition root at startup (by hand, or Spring / a DI container) |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
new / constructing details inside business classes | untestable without the real infrastructure | inject them through the constructor |
| An interface for every class, including pure helpers | indirection with no second implementation or test need | abstract only volatile details |
Abstractions that leak details (executeSql, driver exceptions) | the business code still depends on the database | speak the domain's language; translate errors in the adapter |
| A service locator called inside methods | hidden dependencies | constructor parameters |
| The interface defined by the infrastructure side | dependencies still point at the details | the business side owns the interface |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. Why is new SmtpMailer() inside OrderService a DIP violation, and what's the fix?
Show answer
The high-level service depends directly on a low-level detail. Define a Notifier interface (owned by the service's side), implement it with an SMTP adapter, and inject it through the constructor.
2. How does DIP make code with timeouts or expiry dates easier to test?
Show answer
Treat the clock as a dependency: inject a Clock interface instead of calling Instant.now(), and tests can set or advance time deterministically.
3. Who should own the interface - the database layer or the business logic?
Show answer
The business logic (high-level module). The database adapter implements the interface the domain defined - that's the "inversion".
Key takeaways
- Business rules depend on abstractions they own; details implement them.
- Inject dependencies through constructors; wire them in one place.
- Clocks, databases, HTTP clients and file systems are all details worth abstracting.
- Don't wrap stable, dependency-free code in interfaces just because.