Refactoring Code to Follow DIP
Refactoring Code to Follow DIP
The Dependency Inversion Principle (DIP) is the D in SOLID, and it is the one that most directly shapes how testable, swappable, and layered your code becomes. This lesson is about the refactoring move: you start with a class that reaches down and grabs a concrete collaborator, and you invert that relationship so both sides depend on an abstraction instead.
1. Intuition — the problem it solves
In naive layering, high-level policy code (business logic) directly news or imports low-level detail code (a specific database driver, a specific SMTP library, a specific HTTP client). The arrow of dependency points from important code to unimportant code. That is backwards. Your core domain logic — the thing that rarely changes and is expensive to get wrong — ends up recompiling, breaking, and being untestable every time a volatile detail like "which mail vendor" changes.
DIP flips the arrow. The high-level module declares what it needs as an interface it owns, and the low-level detail is forced to conform to that interface. Now the stable code depends on nothing volatile, and the volatile detail is a plug-in. This is why it is called dependency inversion: the source-code dependency now points against the flow of control, from detail toward policy.
2. Precise definition
DIP has two clauses:
- 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.
Two clarifications interviewers love. First, an "abstraction" here means an interface or abstract type, not merely "a class that feels abstract." Second — and this is the subtle part — the abstraction should be owned by the client (the high-level module), not by the implementer. The interface expresses the consumer's need, phrased in the domain's language. This is what separates DIP from just "code to an interface."
DIP is enabled by Dependency Injection (DI) but they are not the same thing. DIP is the design goal (which way the dependency arrows point); DI is a mechanism (passing the collaborator in via constructor/setter/parameter) for wiring it up. You can do DI badly and still violate DIP by injecting a concrete type.
3. Concrete example — the refactoring
Before: the high-level OrderService hard-codes a low-level detail.
// VIOLATION: policy depends on a concrete detail
class OrderService {
private final PostgresOrderRepo repo = new PostgresOrderRepo();
private final SmtpMailer mailer = new SmtpMailer("smtp.acme.com");
void place(Order o) {
repo.insert(o); // tied to Postgres
mailer.send(o.email(), ...); // tied to SMTP
}
}After: the service depends only on abstractions it defines, and details are injected.
// Abstractions OWNED by the high-level module (domain language)
interface OrderRepository { void save(Order o); }
interface Notifier { void notify(Order o); }
class OrderService {
private final OrderRepository repo;
private final Notifier notifier;
OrderService(OrderRepository repo, Notifier notifier) { // DI
this.repo = repo;
this.notifier = notifier;
}
void place(Order o) {
repo.save(o);
notifier.notify(o);
}
}
// Details DEPEND ON the abstraction (implement it)
class PostgresOrderRepo implements OrderRepository { /* ... */ }
class SesNotifier implements Notifier { /* ... */ }When it applies: the moment a class contains a new SomeConcreteInfrastructure(), a static call into a vendor SDK, or an import that crosses from your domain into a specific technology. Those are the seams where you invert.
4. When to use / when NOT
Use it when a dependency is volatile (third-party SDKs, I/O, clocks, randomness), when you need to substitute implementations for testing (a fake repo, an in-memory notifier), or when you are drawing an architectural boundary — DIP is the mechanism that lets Clean/Hexagonal architectures keep the domain at the center with infrastructure on the outside as adapters.
Do NOT reflexively invert everything. The named alternative is simply depending on the concrete class directly, and that is correct when the dependency is stable: String, ArrayList, a pure value object, or a stable standard-library type. Inverting those buys you nothing and costs you a wasted interface. The trade-off is real: every abstraction adds an indirection you must navigate, a file to open, and a mental hop when reading the code. An interface with exactly one implementation that will never gain a second is often speculative generality — a YAGNI smell. Invert on the axis of expected change, not everywhere.
5. Pitfalls interviewers probe
- "DIP vs DI vs IoC." Be crisp: DIP = which way arrows point (principle); DI = passing collaborators in (technique); Inversion of Control = the framework calling you rather than you calling it (broader style). Conflating them is the classic tell of a shallow answer.
- Who owns the interface? If
OrderRepositorylives in the persistence package next to Postgres, you have merely "coded to an interface" — the domain still points at infrastructure. True DIP puts the interface in the domain layer. Package/module placement is the proof. - Leaky abstractions. If the interface exposes
ResultSet, JPA entities, or vendor exceptions, the detail has bled through and inversion is cosmetic. The abstraction must speak the domain's language, not the technology's. - Interface-per-class explosion. Wrapping every class in an
IFoois a smell, not DIP. Interviewers check that you invert selectively. - Where does
newlive now? Something must still choose concretes — the composition root (main/DI container) at the application's entry point. Candidates who can't say where wiring happens reveal they've only read about it.
Key takeaways
- DIP inverts the source-code dependency so both policy and detail depend on an abstraction, pointing the arrow against the flow of control.
- The abstraction must be owned by the high-level module and phrased in domain language — placement in the domain package is the real test.
- DIP is the goal; DI is the mechanism; IoC is the broader style — keep the three distinct.
- Invert on the axis of volatility; depend directly on stable types and avoid one-implementation interfaces (YAGNI).
- Concretes are still created — at the composition root; leaky abstractions and interface-per-class explosion are the failure modes interviewers hunt for.
🤖 Don't fully get this? Learn it with Claude
Stuck on Refactoring Code to Follow DIP? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Refactoring Code to Follow DIP** (OO & Low-Level Design) and want to truly understand it. Explain Refactoring Code to Follow DIP from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Refactoring Code to Follow DIP** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Refactoring Code to Follow DIP** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Refactoring Code to Follow DIP** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.