Dependency Injection in Java
Constructor injection, inversion of control, testability.
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 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();
}
}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());
}Hello Good morningGood morning HelloHello 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
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.
class Checkout {
private final PaymentGateway gateway;
Checkout(PaymentGateway gateway) {
this.gateway = gateway;
}
}Visible in the signature, final, and works in a plain unit test.
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.
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
- Constructor injection: explicit, final, testable
- IoC: a container or main() builds and wires the objects
- Tests pass fakes through the constructor
- Field injection hides dependencies and blocks final fields
💡 A camera takes whatever lens you mount; it doesn't weld one on at the factory.
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());
}- Morning
- Hi
- 9
- 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)?
- Field injection doesn't work with interfaces
- Constructor injection makes startup faster
- Dependencies are visible in the signature, can be final, and the class works in a plain unit test
- 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.