DI is used often and confused often. “DI = IoC,” “the DI Container is DIP,” “using Spring satisfies DIP” — these equivalences appear regularly in learning material and blog posts. The three are not in the same position; they sit at different levels of abstraction, and without seeing that hierarchy clearly it is easy to confuse framework features with design principles.

DIP is a principle — “high-level modules do not depend on low-level modules.” IoC is a pattern — control flow is handed to something outside the caller. DI is a technique — dependencies are received from outside. Below them sits the DI Container, a tool. The principle is the most abstract; the pattern implements the principle; the technique implements the pattern; the tool automates the technique.

flowchart TB
  A[DIP
Design Principle] --> B[IoC
Control Pattern] B --> C[DI
Injection Technique] C --> D[DI Container
Automation Tool]

DIP

The direction of dependencies between modules is what DIP addresses. It is one of the five SOLID principles, and the definition reduces to two lines.

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

The classic violation is business logic depending directly on data-access technology.

class OrderService {
  private final MySQLPaymentRepository repository = new MySQLPaymentRepository();

  public void charge(Order order) {
    repository.save(order.payment());
  }
}

OrderService knows about a concrete class, MySQLPaymentRepository. Switching the DB to PostgreSQL or swapping in a mock for tests requires editing OrderService. The dependency direction flows from high-level (business) to low-level (data access).

Applying DIP reverses the direction by placing an abstraction between them.

interface PaymentRepository {
  void save(Payment payment);
}

class OrderService {
  private final PaymentRepository repository;

  public OrderService(PaymentRepository repository) {
    this.repository = repository;
  }

  public void charge(Order order) {
    repository.save(order.payment());
  }
}

class MySQLPaymentRepository implements PaymentRepository { /* ... */ }

On the surface this only added an interface, but the core point is who owns the interface. PaymentRepository is defined by the high-level module (the business layer). The low-level module (the infrastructure layer) implements that interface. The direction flips — the low-level depends on the abstraction owned by the high-level. That is the inversion.

Adding an interface alone does not satisfy DIP if the infrastructure layer owns the interface. The dependency direction remains unchanged. DIP is a principle about the location of the interface, not its existence.

IoC

“Who calls whom” is the control direction IoC addresses. In typical code I call library functions. With IoC applied, the framework calls my code. The Hollywood Principle — “Don’t call us, we’ll call you” — is the often-cited slogan.

IoC is one way to satisfy DIP. When control flows from framework to my code, my code does not construct its own dependencies. The framework constructs and hands them over. Depending on abstractions becomes natural.

IoC has more than one implementation.

  • Dependency Injection. Dependencies are received from outside.
  • Service Locator. Dependencies are fetched from a central registry.
  • Template Method. A parent class defines the flow; subclasses fill in specific steps.
  • Event-driven. Registered handlers run when an event occurs.

DI is the most explicit and most frequently used of these. That is why “IoC = DI” is a common equivalence, but it is not accurate.

DI

Objects receiving their dependencies from outside instead of constructing them is the DI technique. Injection style varies by where the dependency is received.

  • Constructor Injection. Dependencies are received as constructor parameters. Natural when dependencies are required and immutable. The most commonly recommended form.
  • Setter Injection. Dependencies are received through setter methods. Used when dependencies are optional or may change at runtime. The trade-off is that the object can briefly exist without its dependencies.
  • Interface Injection. A separate interface is defined to receive the dependency. Rarely used in practice.

Constructor Injection is recommended because it expresses immutability and required-ness. A constructor-injected dependency can be declared final, and the object cannot exist without its dependencies. Setter injection guarantees neither.

DI is the concrete implementation of IoC and, at the same time, a natural path to satisfying DIP. When the constructor receives an abstraction (interface), the object depends only on the abstraction without knowing the concrete implementation.

DI Container

Hand-wiring accumulates boilerplate as the dependency graph grows. The DI Container automates that wiring.

// DI by hand
PaymentRepository repository = new MySQLPaymentRepository();
OrderService orderService = new OrderService(repository);
PaymentController controller = new PaymentController(orderService);

// DI Container automating
@Service
class OrderService {
  public OrderService(PaymentRepository repository) { /* ... */ }
}

The automation is misread in two directions.

DIP, IoC, and DI are all satisfied without a DI Container. Hand-wiring code in which the high-level module owns the interface and dependencies are injected via the constructor satisfies all three. In small systems, hand-wiring is enough.

Adopting a DI Container alone does not satisfy DIP. Even with @Autowired annotations everywhere, if the interface partitioning is wrong — for instance, the business layer depending on an interface named Repository owned by the infrastructure layer — the dependency direction still flows from high-level to low-level. The DI Container is a tool, not a designer.

The Hierarchy

LevelNameIdentityExample
PrincipleDIPDesign principle“High-level does not depend on low-level”
PatternIoCControl flow“The framework calls my code”
TechniqueDIDependency delivery“Inject through the constructor”
ToolDI ContainerAutomationSpring @Autowired, NestJS @Injectable

Correcting Common Equivalences

Seeing the hierarchy clearly exposes problems in equivalences that appear frequently.

“DI = IoC.” DI is one implementation of IoC. Service Locator, Template Method, and Event-driven also satisfy IoC. Treating the two as identical traps thinking in the surface trait “the framework constructs objects” and hides the other shapes IoC can take.

“Using a DI Container satisfies DIP.” When the interface’s location is wrong, the DI Container is meaningless. If the business layer depends on an interface owned by the infrastructure layer, automatic injection leaves the dependency direction unchanged. DIP is a principle about structure, not about wiring method.

“Using Spring automatically gives DIP.” Frameworks make it easy to satisfy DIP, but they do not satisfy it automatically. Interface partitioning, module boundaries, and dependency direction remain the designer’s responsibility. The framework only provides a convenient environment for expressing those decisions in code.

Principle, pattern, technique, and tool sit at different levels. Flattening them invites the complacency of “the framework took care of it.” DIP is the result of design, not the result of tools. Tools only help make that result easier to produce.

References