- Low Level Design
- /
- Dependency Inversion Principle
Course content
The Dependency Inversion Principle has two halves. First, high-level modules should not depend on low-level modules. Both should depend on abstractions. Second, abstractions should not depend on details. Details should depend on abstractions.
In practice this means a class that contains business logic should not reach down and grab a database, a network client, or a third-party SDK directly. It should declare an interface that describes what it needs, and somebody else should provide the concrete implementation. The high-level code talks to the abstraction. The low-level code implements the abstraction. Neither side knows about the other directly.
In this module's running example, the checkout backend of an online store (the full tour), the slice for this principle is OrderService, the high-level class that validates and records orders, and the MySQL repository it currently constructs for itself. The page stands on its own if you have not read the tour.
The word "inversion" can be confusing. Without DIP, the natural direction of dependency is high-level code reaching down into low-level code: an OrderService calling new MySqlOrderRepository() inside its body. With DIP, the dependency is inverted so that both the high-level and the low-level code depend on a shared abstraction instead. The high-level code does not import the low-level code at all.
This matters because the high-level code expresses the policy of the application (the business rules, the workflows, the user-visible behaviour). The low-level code expresses mechanism (which database, which mail provider, which queue). Policies tend to be stable. Mechanisms change all the time. Without inversion, every change of mechanism forces a change to the policy code, even though the policy itself has not changed. With inversion, the mechanism is plugged in from outside, so the policy code is untouched.
| Smell | What it looks like | Why it's a problem |
|---|---|---|
| A `new` of a concrete class inside a high-level class | this.repo = new MySqlOrderRepository(); | The high-level class is now bound to a specific implementation |
| A field typed against a concrete class for a dependency | private final MySqlOrderRepository repo; | The type system is recording the wrong contract |
| A no-arg constructor that allocates internally | public OrderService() { this.repo = new ...; } | There is no place to inject a fake or a different real implementation |
| Static calls to concrete utilities from inside business logic | EmailUtil.send(...); MysqlClient.connect(); | Same problem as `new`, dressed up as a static method |
| Tests that run a real database to verify business logic | Spinning up MySQL just to test order validation | The high-level logic is not separable from its low-level dependencies |
OrderService is the high-level module. Its job is to enforce the rule that orders have a positive amount and then to record them. MySqlOrderRepository is a low-level module. It knows about a specific database technology.
The smelly version glues these two together. The field is typed as the concrete repository, and the constructor allocates one. A test that wants to verify the validation rule cannot do so without spinning up a real MySQL instance. A migration to Postgres requires editing the constructor of OrderService, even though the validation logic has not changed. A future caller that wants to push orders to a queue instead of a database has nowhere to plug that change in.
The deeper issue is that the source code of the high-level module depends on the source code of a low-level module. Inversion fixes this by routing both through a shared abstraction.
What this costs in production
OrderService newed up MySqlOrderRepository in its constructor, so the validation logic could not be unit tested without a live MySQL instance. The team skipped those tests, and a regression that let through zero-amount orders shipped unnoticed. When the company later migrated to Postgres, the 'one-line' change rippled into every high-level service that had hardcoded the concrete repository.
OrderService now depends only on the OrderRepository interface. It does not import the MySQL class at all. The constructor declares the dependency and stores the injected reference in a final field. There is no no-arg constructor, so callers cannot forget to provide a repository.
The wiring decision is made at the boundary of the application, often called the composition root. In production, that boundary creates a MySqlOrderRepository and passes it in. In tests, it creates an in-memory or recording repository and passes that in. Either way, OrderService is the same code. None of its tests need a live database, none of its source has to change when the data store changes, and adding a new implementation is purely additive.
This is also why the principle has the word "inversion" in it. Before the refactor, OrderService (high-level) depended on MySqlOrderRepository (low-level). After the refactor, both depend on OrderRepository. The arrow that used to point from policy to mechanism now points the other way: the mechanism implements an abstraction the policy defines.
There is a subtlety in the refactor that separates applying this principle from merely recognising it: which package does OrderRepository belong to? The reflex answer is the persistence package, next to MySqlOrderRepository. The correct answer is the orders package, next to OrderService. The interface is the high-level module's declaration of what it needs, written in the high-level module's vocabulary; the persistence package implements it. That placement is what actually flips the arrow between the packages.
If OrderRepository lived in the persistence package, OrderService would still import persistence, and the arrow would still point from policy to detail. You would have dependency injection without dependency inversion, which is a distinction worth having ready in an interview.
DIP vs DI vs IoC
Dependency inversion is the principle: high-level code owns the abstraction and details implement it. Dependency injection is a technique that helps satisfy it: dependencies arrive from outside instead of being constructed inside. Inversion of control is the umbrella idea that a framework calls your code rather than the other way round; a DI container is one form of it. You can inject everything and still violate DIP if your interfaces live with, and are shaped by, their implementations.
Six months later order volume outgrows the relational database and the team decides to move orders to DynamoDB. In the original design this is the migration from the war story: every service that ever wrote new MySqlOrderRepository() gets edited, and untested business logic rides along in the same release.
In the refactored design, the migration is one new file and one line at the composition root.
What this saves in production
OrderService and every one of its tests are untouched, which is proof the business rules did not move during the migration. The two repositories can even run side by side behind the same interface for a gradual cutover, something the hardcoded version could not express at all.
DIP is not a rule that every dependency must be hidden behind an interface. If a class uses String or ArrayList, those are concrete classes, but you do not need to introduce a StringLike or ListLike abstraction. The principle targets dependencies on volatile or replaceable details: databases, network clients, third-party SDKs, things you plausibly want to swap or fake. Stable platform types stay as they are.
A related trap is introducing an interface for a single implementation that nobody intends to replace. If you have one repository class and the design has no plausible second one, the abstraction is overhead with no payoff. Wait until the second implementation is real (or until tests genuinely need to substitute), then extract the interface. Java is also flexible enough that you can extract the interface later without rewriting callers, so the cost of waiting is small.
The companion exercise gives you an OrderService that hardcodes new MySqlOrderRepository() inside its no-arg constructor. Refactor it so the high-level service depends on an OrderRepository interface and receives its repository through constructor injection. The validator's strict check plants its own implementation of OrderRepository, hands it to your OrderService, and confirms the service actually used what it was given. If OrderService is still constructing its own repository internally, that check fails.
Open the exercise: Dependency Inversion: Inject the OrderRepository.