Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Dependency Inversion Principle
LLD

Dependency Inversion Principle

Learn how high-level modules should depend on abstractions rather than low-level implementation details.

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:

  1. High-level modules should not depend on low-level modules — both depend on abstractions.
  2. 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

ConceptWhat it is
DIPDesign rule about which direction dependencies point (architecture)
DIWiring 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

ConstructorSetterMethodField
Mandatory guarantee✅ compile-enforced❌ null until set✅ per call
ImmutabilityPossible (final)Non/aNo
Test ergonomicsnew Svc(fake) — trivialMust remember settersPass stub per callReflection needed
Circular depsFails fast at startupHides themn/aHides them
Recommended defaultYesOptional deps onlyStateless helpersAvoid

Why Constructor Injection Wins by Default

  • Missing dependency = object cannot exist — invalid states unrepresentable.
  • final fields 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.

My Private Notes

Notes are auto-saved locally to this device.