🏛️ Design Principles & Patterns · Advanced

Dependency Injection in Java

Constructor injection, inversion of control, testability.

🧩 The mysteryYour test needs it to be 9 a.m. on a Monday, but the code reads the real clock. You can't change time... unless the code lets you hand it one.

Hand it in, don't build it

With Dependency Injection, an object receives its collaborators from outside instead of creating them with new. Like a camera that takes whatever lens you mount. Constructor injection is the preferred form: dependencies are explicit, can be final, and the object is complete after construction.

class Checkout {
    private final PaymentGateway gateway;
    Checkout(PaymentGateway gateway) {
        this.gateway = gateway;
    }
}
// test: new Checkout(new FakeGateway())
⚠️ The trap

The hidden new

This service builds its own Clock, so tests can't control time: the constructor's Clock.systemUTC() call blocks it. Fix: take a Clock constructor parameter, pass Clock.systemUTC() in production and Clock.fixed(...) in tests.

class OrderService {
    private final OrderRepo repo;
    private final Clock clock;
    OrderService(OrderRepo repo) {
        this.repo = repo;
        this.clock = Clock.systemUTC();
    }
}
🔮 Predict it

Time on demand

The hour source is injected. What does this print?

record Greeter(IntSupplier hour) {
    String greet() {
        return hour.getAsInt() < 12
            ? "Good morning" : "Hello";
    }
}
void main() {
    IO.println(new Greeter(() -> 15).greet());
    IO.println(new Greeter(() -> 8).greet());
}
  1. Hello Good morning
  2. Good morning Hello
  3. Hello Hello
Show the answer

Each Greeter gets its own fake clock: one always says 15, the other 8. The output doesn't depend on when the code runs, which is exactly what makes it testable.

Inversion of Control, no framework needed

Inversion of Control: something *outside* builds and wires the objects. That place is the composition root, often main() or a framework config. DI doesn't require Spring: the code below is DI. Frameworks just automate the wiring.

void main() {
    var gateway = new StripeGateway();
    var clock = Clock.systemUTC();
    var checkout = new Checkout(
        gateway, clock);
    checkout.run();
}

Field vs constructor injection

✗ Field injection
class Checkout {
    @Autowired
    private PaymentGateway gateway;
}

Hidden dependency, can't be final, needs a container or reflection to build; a missing one shows up later as a NullPointerException.

✓ Constructor injection
class Checkout {
    private final PaymentGateway gateway;
    Checkout(PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

Visible in the signature, final, and works in a plain unit test.

🤔 Think first

Name the parts

In a DI codebase, what is a *test double*, and why does it matter that wiring happens in one composition root?

Think about it, then reveal the answer

A test double is a fake implementation used in tests, passed through the constructor. Keeping all wiring in one composition root leaves every other class free of new for its collaborators, so any of them can be swapped.

💼 In the real world

On real teams

Spring, Quarkus and Micronaut are DI containers, and their docs recommend constructor injection for required dependencies. Code that injects its clock, database and HTTP clients gets fast, reliable unit tests; code that calls new inside gets slow, flaky ones.

Key takeaways

  1. Constructor injection: explicit, final, testable
  2. IoC: a container or main() builds and wires the objects
  3. Tests pass fakes through the constructor
  4. Field injection hides dependencies and blocks final fields

💡 A camera takes whatever lens you mount; it doesn't weld one on at the factory.

🤯 Did you know?

Martin Fowler coined the term Dependency Injection in a 2004 article, to give a clearer name to what containers of the day were calling Inversion of Control.

Practice questions

What does this print?

record Greeter(IntSupplier hour) {
    String greet() {
        int h = hour.getAsInt();
        return h < 12 ? "Morning" : "Hi";
    }
}
void main() {
    var g = new Greeter(() -> 9);
    System.out.println(g.greet());
}
  1. Morning
  2. Hi
  3. 9
  4. Compile error
Check your answer

Morning. The time source is injected, and here it always says 9, so greet() returns "Morning" no matter when the code runs.

Why do many teams prefer constructor injection over field injection (@Autowired on a private field)?

  1. Field injection doesn't work with interfaces
  2. Constructor injection makes startup faster
  3. Dependencies are visible in the signature, can be final, and the class works in a plain unit test
  4. Field injection is forbidden in Java 25
Check your answer

Dependencies are visible in the signature, can be final, and the class works in a plain unit test. With field injection you can't build the object without a container or reflection, and a missing dependency shows up later as a NullPointerException.

Next: rules from Effective Java that prevent whole families of bugs, starting with a record that isn't as immutable as it looks.