Dependency Injection vs Dependency Inversion vs IoC

Introduction

This tutorial explains the difference between Dependency Injection (DI), the Dependency Inversion Principle (DIP) and Inversion of Control (IoC). The three names sound alike, appear together in every Spring tutorial, and are often used as if they meant the same thing. They do not. We will start with a short definition of each and a diagram of how they relate. We will then build one Java example in three steps - hard-wired, then inverted and injected by hand, then wired by a small container - to see each idea in code. Lastly, we will compare the ways of injecting a dependency, and see how this maps to Spring.

Three definitions in one line each

  • Dependency Inversion Principle (DIP) is a design principle about the direction of source-code dependencies: high-level policy should not depend on low-level details; both should depend on abstractions, and the abstraction belongs with the policy. It is the "D" in SOLIDClick to read tutorial: SOLID Principles in Java: Overview with Examples.
  • Inversion of Control (IoC) is a broader principle about who controls the flow of the program: instead of your code calling a library, a framework calls your code. It is also known as the Hollywood principle - "don't call us, we'll call you".
  • Dependency Injection (DI) is a technique, and one form of IoC: an object does not create or look up its dependencies; they are handed to it from outside, usually through its constructor. Martin Fowler named it in 2004, to separate it from other kinds of IoC.
Dependency Injection vs Dependency Inversion Principle vs Inversion of Control - how the three ideas relate
In short: DIP decides where the interfaces go, DI decides how the implementations arrive, and IoC is the wider idea that DI is one example of. You can have any one without the others, which is why it pays to keep them apart.

Dependency Inversion - which way the arrows point

In a naive design, high-level code calls low-level code directly, so it also depends on it at compile time: a payslip service that creates an SmtpMailer cannot be compiled, reused or tested without SMTP code. DIP adds an interface, Mailer, that the payroll code defines for its own needs. The SMTP code then implements that interface, so the compile-time dependency now points from the detail to the policy - the opposite of the call direction. That reversal is the "inversion".
Dependency Inversion Principle - the dependency arrow from PayslipService to SmtpMailer is inverted by a Mailer interface owned by the payroll package
Notice that DIP says nothing about who creates the SmtpMailer. The service could still look one up in a static registry, and the design would follow DIP. Creating and supplying the object is where DI comes in. The Dependency Inversion Principle tutorialClick to read: Dependency Inversion Principle tutorial covers DIP on its own in more depth.

One example in three steps

The code below sends an employee's payslip by email, in three versions: (1) hard-wired, with neither DIP nor DI; (2) with a Mailer interface (DIP) supplied through the constructor (DI), including a test double; and (3) wired by a tiny IoC container, to show what Spring or Guice do for you.
Java code showing hard-wired dependencies, dependency injection and a minimal IoC container
//DependencyWiring.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

public class DependencyWiring {

  record Employee(String name, String email, double netPay) {}

  // ---------- 1. Hard-wired: no DIP, no DI ----------
  static class SmtpMailerV1 {
    void send(String to, String body) {
      System.out.println("SMTP -> " + to + ": " + body);
    }
  }

  static class PayslipServiceV1 {
    private final SmtpMailerV1 mailer = new SmtpMailerV1();   // creates its own detail
    void sendPayslip(Employee e) { mailer.send(e.email(), "Net pay " + e.netPay()); }
  }

  // ---------- 2. DIP: the policy owns an abstraction... ----------
  interface Mailer { void send(String to, String body); }

  static class PayslipService {
    private final Mailer mailer;

    PayslipService(Mailer mailer) { this.mailer = mailer; }  // ...and DI: it is handed in

    void sendPayslip(Employee e) {
      mailer.send(e.email(), String.format("Net pay for %s: %.2f", e.name(), e.netPay()));
    }
  }

  static class SmtpMailer implements Mailer {
    public void send(String to, String body) {
      System.out.println("SMTP -> " + to + ": " + body);
    }
  }

  // A test double: records messages instead of sending them
  static class RecordingMailer implements Mailer {
    final List<String> sent = new ArrayList<>();
    public void send(String to, String body) { sent.add(to + " | " + body); }
  }

  // ---------- 3. A tiny IoC container ----------
  static class Container {
    private final Map<Class<?>, Supplier<?>> providers = new HashMap<>();

    <T> void register(Class<T> type, Supplier<? extends T> provider) {
      providers.put(type, provider);
    }

    <T> T get(Class<T> type) { return type.cast(providers.get(type).get()); }
  }

  public static void main(String[] args) {
    Employee tom = new Employee("Tom Jones", "tom.jones@example.com", 4310.50);

    System.out.println("1. Hard-wired:");
    new PayslipServiceV1().sendPayslip(tom);

    System.out.println("2. Injected by hand in main(), the composition root:");
    new PayslipService(new SmtpMailer()).sendPayslip(tom);

    System.out.println("   The same service with a test double:");
    RecordingMailer recorder = new RecordingMailer();
    new PayslipService(recorder).sendPayslip(tom);
    System.out.println("   recorded: " + recorder.sent);

    System.out.println("3. Wired by a container:");
    Container container = new Container();
    container.register(Mailer.class, SmtpMailer::new);
    container.register(PayslipService.class,
        () -> new PayslipService(container.get(Mailer.class)));
    container.get(PayslipService.class).sendPayslip(tom);
  }
}
 OUTPUT of the above code
1. Hard-wired:
SMTP -> tom.jones@example.com: Net pay 4310.5
2. Injected by hand in main(), the composition root:
SMTP -> tom.jones@example.com: Net pay for Tom Jones: 4310.50
   The same service with a test double:
   recorded: [tom.jones@example.com | Net pay for Tom Jones: 4310.50]
3. Wired by a container:
SMTP -> tom.jones@example.com: Net pay for Tom Jones: 4310.50

Explanation of the code

  • Step 1: PayslipServiceV1 creates its SmtpMailerV1 in a field initializer. It depends on a concrete class (no DIP), and it controls the creation itself (no DI). There is no way to run it without sending a real email.
  • Step 2: PayslipService depends only on the Mailer interface - that is DIP. It receives the implementation through its constructor - that is DI. The objects are created and connected in main(), the composition root, which is the one place that knows about every concrete class.
  • Because of DI, the very same service class runs with RecordingMailer, and the output shows the message it would have sent. This is the main practical payoff: code can be tested without its real infrastructure.
  • Step 3: Container maps each type to a recipe (Supplier) for building it. Our code no longer decides when PayslipService or its Mailer are created; the container does, and calls our constructors. That is Inversion of Control applied to object creation - real containers such as Spring read the recipes from annotations or configuration, and also manage scopes and lifecycles.
  • Steps 2 and 3 produce the same result. A container is a convenience for large applications, not a requirement for DI - DI by hand is still DI.

Inversion of Control beyond DI

IoC is much older than DI frameworks, and you use it all the time in plain Java. Whenever you hand your code to something else and it decides when to call it, control is inverted -
Everyday Inversion of Control in the JDK
// You never call compare(); List.sort() calls it, as often and in the order it chooses
employees.sort(Comparator.comparing(Employee::name));

// You never call run(); the executor calls it, on a thread it chooses
executor.submit(() -> sendPayslip(tom));

// You never call actionPerformed(); the Swing event loop calls it when a button is clicked
button.addActionListener(event -> refreshReport());
Callbacks, event listeners, and the Template Method patternClick to read: Template Method pattern (where a framework base class calls your overridden methods) are all forms of IoC. DI is the form that applies it to creating and connecting objects.

Ways to inject a dependency

There are three common ways to hand a dependency to an object -
Constructor, setter and field injection
// Constructor injection - preferred
class PayslipService {
  private final Mailer mailer;
  PayslipService(Mailer mailer) { this.mailer = Objects.requireNonNull(mailer); }
}

// Setter injection - for optional dependencies that have a sensible default
class ReportService {
  private Clock clock = Clock.systemUTC();
  void setClock(Clock clock) { this.clock = clock; }
}

// Field injection - a framework sets a private field by reflection
class AuditService {
  @Autowired private Mailer mailer;   // hidden dependency; hard to set in a plain unit test
}
  • Constructor injection makes dependencies required, visible in the signature, and final. An object cannot exist without them, and a long constructor is an honest sign that the class does too much.
  • Setter injection suits optional dependencies with a default, such as a Clock. The object is usable before the setter is called.
  • Field injection is the shortest to write but hides dependencies, prevents final fields, and ties the class to the framework that sets them. Most teams avoid it outside tests.

DI versus the Service Locator

A service locator is the other common way to decouple a class from concrete implementations: the class asks a registry for what it needs, for example ServiceLocator.get(Mailer.class). It satisfies DIP, since the class depends only on Mailer, but it is not DI - the class still fetches its own dependency, and the dependency no longer appears in its constructor. Readers and tests have to know which entries the registry must contain. DI keeps dependencies explicit, which is why it is usually preferred.

How this maps to Spring

In Spring, the application context is the IoC container, beans are the objects it creates, and constructor injection is the recommended style -
The same example in Spring
public interface Mailer { void send(String to, String body); }

@Component
public class SmtpMailer implements Mailer { ... }

@Service
public class PayslipService {
  private final Mailer mailer;

  public PayslipService(Mailer mailer) {   // Spring finds the Mailer bean and passes it in
    this.mailer = mailer;
  }
}
PayslipService has no Spring-specific code in its constructor; with a single constructor, Spring injects it without an @Autowired annotation. The class therefore stays a plain Java class that can be unit-tested with a RecordingMailer, exactly as in step 2.

Summary

In this tutorial we first defined the three terms: DIP is a design principle about which way dependencies point, IoC is the wider principle of letting a framework call your code, and DI is the IoC technique of handing an object its dependencies. Next we saw DIP invert the dependency arrow between a payslip service and its mailer. We then built one example in three steps - hard-wired, injected by hand with a test double, and wired by a tiny container. We then looked at IoC in everyday JDK code, compared constructor, setter and field injection, and contrasted DI with a service locator. Lastly, we saw how the same design looks in Spring.