Dependency Inversion Principle (DIP)
The Problem It Solves
Notification logic (high-level policy: “tell the user”) directly instantiates SmtpEmailer (low-level detail). The dependency arrow — and therefore the control of change — points downward into infrastructure. Consequences: swapping to SMS edits policy code; testing notification requires a live SMTP; infra teams’ release cadence gates business logic.
DIP (Robert C. Martin) has two parts:
- High-level modules should not depend on low-level modules — both depend on abstractions.
- Abstractions should not depend on details — details depend on abstractions.
The Inversion
NAIVE DEPENDENCY INVERTED
┌──────────────────┐ ┌──────────────────┐
│ NotificationSvc │ │ NotificationSvc │
│ new SmtpEmailer │ │ uses Notifier ◄─┼──┐ (interface owned by
└────────┬─────────┘ └──────────────────┘ │ the POLICY layer)
▼ │
┌──────────────────┐ ┌─────┴────────────┐
│ SmtpEmailer │ │ SmtpEmailer impls│
│ (details rule) │ │ SmsNotifier │
└──────────────────┘ └──────────────────┘
policy depends on detail detail depends on policy's abstraction
Right side: the interface lives with the high-level module and speaks its vocabulary (notify(User, Message)), while infrastructure implements it. The compile-time arrow reversed — that is the “inversion,” not merely indirection.
Mechanics
// abstraction owned by policy layer, named in domain terms
interface Notifier { void notify(User user, Message msg); }
class NotificationService { // high-level: stable
private final Notifier notifier;
NotificationService(Notifier n) { this.notifier = n; }
void orderConfirmed(Order o) {
notifier.notify(o.user(), Message.of("Order confirmed"));
}
}
class SmtpEmailer implements Notifier { ... } // low-level: replaceable detail
Swapping email→SMS: new implementation class, zero policy edits, tests inject a fake Notifier.
DIP ≠ Dependency Injection
| Concept | What it is |
|---|---|
| DIP | Design rule about which direction dependencies point (architecture) |
| DI | Wiring technique supplying implementations from outside (mechanism) |
You can do DI while violating DIP (injecting concrete classes) and do DIP with manual wiring (constructor takes interface, main news up the impl). Interviews conflate them constantly — distinguishing them scores.
Historical Proof: JDBC
Applications code against Connection/PreparedStatement interfaces; vendors ship drivers implementing them. Decades of database migrations without application rewrites — DIP at ecosystem scale, interface defined by the policy side (applications), implemented by details (drivers).
Trade-offs & Failure Modes
- Every abstraction costs reading effort; inverting a never-changing detail is ceremony.
- Abstraction shaped like the vendor SDK (“leaky port”) fails its purpose — the contract must speak policy vocabulary.
- Over-inversion produces interface-for-everything soup; invert at genuine variation/testing boundaries only.
Interview Framing
- Drawing both dependency diagrams and naming which arrows reversed is the expected answer.
- Senior signal: locating interface ownership (“the abstraction belongs to the high-level module”) — most candidates place it with the implementation.
What DI Actually Does
DIP says depend on abstractions; DI is the delivery mechanism — someone outside the class supplies (injects) its collaborators instead of the class constructing them. The class declares what it needs; wiring happens at composition time. That separation turns every collaborator into a test seam.
The Four Techniques
// 1. CONSTRUCTOR — dependencies mandatory, immutable, complete at birth
class Checkout {
private final PaymentGateway gateway;
Checkout(PaymentGateway g) { this.gateway = g; }
}
// 2. SETTER — optional/reconfigurable dependencies
class ReportJob {
private Mailer mailer;
void setMailer(Mailer m) { this.mailer = m; }
}
// 3. METHOD — per-call dependency, no retained state
Report render(Data d, Formatter f) { return f.format(d); }
// 4. FIELD (@Autowired directly on field) — container-only
class LegacyService {
@Autowired private PaymentGateway gateway; // hidden, mutable
}
Technique Trade-offs
| Constructor | Setter | Method | Field | |
|---|---|---|---|---|
| Mandatory guarantee | ✅ compile-enforced | ❌ null until set | ✅ per call | ❌ |
| Immutability | Possible (final) | No | n/a | No |
| Test ergonomics | new Svc(fake) — trivial | Must remember setters | Pass stub per call | Reflection needed |
| Circular deps | Fails fast at startup | Hides them | n/a | Hides them |
| Recommended default | Yes | Optional deps only | Stateless helpers | Avoid |
Why Constructor Injection Wins by Default
- Missing dependency = object cannot exist — invalid states unrepresentable.
finalfields enable thread-safe publication.- A constructor needing eleven parameters is a visible SRP alarm; hidden field injection masks the same rot.
Manual vs Framework Wiring
// manual composition root — plain code, full visibility:
public static void main(String[] args) {
PaymentGateway gw = new StripeGateway(config);
Notifier notifier = new SmtpEmailer(mailConfig);
var checkout = new Checkout(gw, notifier); // graph assembled here
}
// framework equivalent (Spring):
@Service class Checkout {
Checkout(PaymentGateway gw, Notifier n) { ... } // container resolves impls
}
Frameworks earn their keep with large graphs, lifecycle scopes, and cross-cutting config. Small apps compose fine in main — zero magic, instant startup.
Production Concerns
- Circular dependency: A needs B needs A → constructor injection fails fast (“requested bean is currently in creation”) — that failure is diagnostic gold; setter/field injection silently permits the cycle and defers the NPE to runtime. Fix the design (extract shared logic), not the injection style.
- Composition root discipline: implementations should be named only in one place; scattering
new StripeGateway()across services reintroduces the coupling DIP removed. - Scope awareness: framework singletons injected into request-scoped objects need proxies — a real source of subtle production bugs.
Interview Framing
- “Why constructor over field injection?” answered via testability + fail-fast + immutability covers the expected ground.
- Senior signal: naming the composition root as the one place allowed to know concrete classes.
Premium Content
Unlock Dependency Inversion Principle and all premium lessons with a subscription.
From ₹199.99/year — See plans