Dependency Inversion Principle (DIP)

Depend on abstractions, not concretions. Where DIP, IoC, and constructor injection are the same idea wearing three names.

Concept Intermediate
9 min read
solid dip abstraction dependency-injection

Summary#

The Dependency Inversion Principle is the D in SOLID and the one most often confused with its mechanism. Robert Martin’s formulation, in two clauses:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

Said plainly: the direction of the source-code dependency between a policy (high-level) and a mechanism (low-level) should be the opposite of the runtime call direction. Policy still calls mechanism at runtime — but in source, both refer to an interface that policy owns and mechanism implements. That is the inversion the principle names.

DIP, inversion of control (IoC), and dependency injection (DI, usually constructor injection) are three names for the same idea at three altitudes:

  • DIP is the principle: depend on abstractions.
  • IoC is the architectural pattern: a high-level module hands off “who provides this” to its caller (or to a container).
  • DI (in particular constructor injection) is the most common technique that implements IoC, which in turn satisfies DIP.

People interchange them and it is mostly fine — but knowing the layering keeps the conversation honest.

Why it matters#

DIP is the principle that lets the rest of SOLID compose into a testable, swappable system. Without it, every class that needs a database, an HTTP client, a clock, or a random number generator reaches out and instantiates one — and the system becomes a tree of hard-wired concrete dependencies that is exactly as testable as production wiring.

Three concrete reasons interviewers care:

  • Testability becomes a property of the design, not a heroic effort. A class that depends on a Clock interface accepts a fake clock in tests. A class that calls System.currentTimeMillis() directly does not — and the test has to resort to mocking statics or running real time, both of which are expensive.
  • The high-level module survives technology changes. A business rule that depends on UserRepository (an interface it owns) survives a migration from MySQL to DynamoDB. A business rule that imports MysqlConnection does not.
  • Bounded contexts can be wired separately. Independent teams can ship a new implementation of an existing abstraction without coordinating with the team that wrote the policy. This is OCP’s plugin story, made structurally honest by DIP.

The principle’s value is measured at the import graph level: are the innermost modules of the system free of imports from infrastructure? If yes, DIP holds. If OrderService imports JdbcDataSource, the inversion has not happened.

How it works#

Spotting the violation#

The most reliable detector is the high-level module that imports a concrete low-level type. A business rule should not name a specific database driver, file format library, or HTTP client by class.

A worked example. A draft order-confirmation policy:

public final class OrderConfirmation {
private final MysqlConnection db = new MysqlConnection("jdbc:mysql://prod/orders");
private final SmtpClient mailer = new SmtpClient("smtp.example.com", 587);
public void confirm(long orderId) {
Order o = db.query("SELECT * FROM orders WHERE id = ?", orderId);
mailer.send(o.email(), "Order confirmed", "Your order " + orderId + " is on its way.");
}
}

Two violations stacked on top of each other:

  1. Source direction. The high-level OrderConfirmation class — which encodes what it means to confirm an order — imports two concrete low-level types: a MySQL driver and an SMTP client. A change to either propagates to the policy.
  2. Instantiation in place. OrderConfirmation constructs its collaborators. There is no seam for a test to inject a fake. There is no way to swap the mailer for a queue without editing this file.

The two violations are usually linked but are not the same. A class can import an abstraction and still violate DIP by new-ing up the implementation. A class can inject its dependencies through the constructor and still violate DIP if those dependencies are concrete types.

The refactor#

Three moves, in order:

1. Define abstractions owned by the policy. OrderConfirmation declares the interfaces it needs, in its own package. Note the direction — the interfaces live with the policy, not with the implementations.

// In the orders / policy package — owned by the high-level module.
public interface OrderRepository {
Order findById(long orderId);
}
public interface OrderMailer {
void sendConfirmation(String email, long orderId);
}

2. Inject the abstractions, do not construct them. The policy receives its collaborators; it does not create them.

public final class OrderConfirmation {
private final OrderRepository orders;
private final OrderMailer mailer;
public OrderConfirmation(OrderRepository orders, OrderMailer mailer) {
this.orders = orders;
this.mailer = mailer;
}
public void confirm(long orderId) {
Order o = orders.findById(orderId);
mailer.sendConfirmation(o.email(), orderId);
}
}

3. Implementations live in the infrastructure layer, and they depend up on the policy’s abstractions.

// In the infrastructure / mysql package — depends on the policy package.
public final class MysqlOrderRepository implements OrderRepository {
private final MysqlConnection db;
public MysqlOrderRepository(MysqlConnection db) { this.db = db; }
@Override public Order findById(long orderId) {
return db.query("SELECT * FROM orders WHERE id = ?", orderId);
}
}
public final class SmtpOrderMailer implements OrderMailer {
private final SmtpClient smtp;
public SmtpOrderMailer(SmtpClient smtp) { this.smtp = smtp; }
@Override public void sendConfirmation(String email, long orderId) {
smtp.send(email, "Order confirmed", "Your order " + orderId + " is on its way.");
}
}

The arrows in the source-code dependency graph now point from infrastructure to policy. At runtime, policy still calls infrastructure — but the source-code dependency has been inverted. That is the principle, made literal.

A composition root — main, a Spring @Configuration class, a Guice module, or a hand-rolled factory — wires it together at the edge of the system:

public static void main(String[] args) {
MysqlConnection conn = new MysqlConnection("jdbc:mysql://prod/orders");
SmtpClient smtp = new SmtpClient("smtp.example.com", 587);
OrderConfirmation policy = new OrderConfirmation(
new MysqlOrderRepository(conn),
new SmtpOrderMailer(smtp)
);
policy.confirm(42L);
}

The composition root is the only place that knows the concrete types. Everything inside the system knows abstractions.

Constructor injection vs the alternatives#

Constructor injection is the default for a reason: it makes dependencies visible in the type signature, the compiler refuses to let you forget one, and the object is fully constructed by the end of its constructor. The alternatives have well-known costs:

Constructor injection. Dependencies are mandatory. Object is immutable. The class is testable without a framework. The downside is a long parameter list when a class has many collaborators — which is also a hint that SRP is being violated.

Field / setter injection. Dependencies are optional and mutable. The class can be constructed without them, then mutated to be valid. A framework typically populates the fields by reflection. Brittle to refactor; objects can be observed in a half-built state.

In an interview, prefer constructor injection unless asked. Bring up field injection only to name its cost.

The other side — when DIP has been over-applied#

The opposite failure mode is abstractions for one: introducing an interface in front of every collaborator regardless of whether it will ever be swapped.

The signal: an interface with one production implementation, used only to satisfy a test mock, with no plausible second implementation in the backlog. If the test mock is the only client of the abstraction, you have not inverted a dependency — you have added one. Consider a hand-written fake instead of an interface, or a more direct testing seam.

DIP is not “interface everything.” DIP is “where a real seam exists between policy and mechanism, point the source arrows the right way.”

Variants and trade-offs#

A few useful refinements:

  • DIP, IoC, DI — same idea, three altitudes. DIP is the principle (depend on abstractions). IoC is the architectural choice (someone else hands the dependency to me). DI is the technique (the someone else is usually a container or a main). A monitor that swaps a fake clock into a class under test is doing all three.
  • The abstraction’s direction of ownership matters more than its existence. A UserRepository interface that lives next to MysqlUserRepository is barely an inversion — both move together. A UserRepository interface in the domain package, with the MySQL adapter importing it from across the layer boundary, is the real inversion.
  • DIP needs ISP to be honest. If the abstraction the policy depends on is a fat interface, the policy is coupled to every method, defeating the inversion. ISP keeps DIP’s abstractions tight.
Wiring approachWhen it fitsRisk
Hand-rolled composition in mainSmall services, no framework, easy to followComposition root grows; eventually a main of 200 lines
Framework container (Spring, Guice, Dagger)Larger systems with many bindings, scopes, lifecyclesMagic at startup; the dependency graph is no longer in source
Service LocatorLegacy code where constructor change is painfulHides dependencies; LSP-style surprises at runtime when a lookup fails

When this is asked in interviews#

Three moves carry DIP discussion in an LLD round:

  • State the inversion explicitly. Saying “the UserService depends on a UserRepository interface, and MysqlUserRepository implements that interface from the infrastructure layer — so the source arrow points from infrastructure to domain” is the senior phrasing. Many candidates say “I use interfaces” and stop; the senior version names the direction.
  • Show the composition root. Sketch a main (or a Spring @Configuration) that wires the concrete tree. Interviewers want to see that you know where the magic happens and that nothing inside the policy is magical.
  • Defend not abstracting. If the interviewer adds a small utility — a UUID generator, a String formatter — resist wrapping it. Knowing where DIP is worth its cost and where it is not is the senior signal.

The principle’s one durable test, after the meeting: can the policy module compile and run with all its concrete infrastructure dependencies removed and replaced with fakes? If yes, DIP holds. If the policy imports pull infrastructure with them, the arrows are still pointing the wrong way.

Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.