Dependency Injection in Practice
Dependency Injection is the mechanism that makes IoC concrete: the container injects collaborators into your class instead of you fetching them.
Introduction
Dependency Injection is the mechanism that makes IoC concrete: the container injects collaborators into your class instead of you fetching them. Spring supports three injection styles — constructor, setter, and field — and the right choice matters more than people think.
Good DI usage produces classes that feel like plain Java: you can new them up in a unit test, pass in fakes, and assert behaviour without starting Spring. Bad DI usage gives you classes that only work "by magic" and that nobody dares to refactor.
Understanding the topic
When to use each style:
- Constructor injection — for required dependencies. Makes the class immutable and easy to test with plain
new. - Setter injection — for optional dependencies that may change after construction.
- Field injection — convenient but hides dependencies and breaks unit testing without Spring. Avoid in new code.
Wiring tools you will see: @Autowired (optional on a single constructor since Spring 4.3), @Qualifier("name") to disambiguate when multiple beans match a type, and @Primary to mark a default choice.
Syntax reference
Constructor injection (recommended):
@Servicepublic class InvoiceService {private final PaymentGateway gateway;private final InvoiceRepository repo;// Since Spring 4.3, @Autowired is optional on a single constructor.public InvoiceService(PaymentGateway gateway, InvoiceRepository repo) {this.gateway = gateway;this.repo = repo;}}
Disambiguating when several beans match a type:
interface Notifier { void send(String to, String msg); }@Component("email") class EmailNotifier implements Notifier { /*...*/ }@Component("sms") class SmsNotifier implements Notifier { /*...*/ }@Serviceclass AlertService {private final Notifier notifier;AlertService(@Qualifier("sms") Notifier notifier) { this.notifier = notifier; }}
Informative example
Testing without Spring becomes trivial:
class InvoiceServiceTest {@Testvoid charges_and_persists() {var gateway = new FakeGateway();var repo = new InMemoryInvoiceRepo();var service = new InvoiceService(gateway, repo);service.charge(new Order("o-1", new Money("USD", 49_00)));assertTrue(gateway.lastChargeWas(49_00));assertEquals(1, repo.count());}}
Notice there is no @SpringBootTest, no annotations, no application context. That speed is the practical payoff of constructor injection.
Real-world use
Teams that adopt constructor injection consistently report fewer NullPointerExceptions, faster test suites and easier onboarding — because every class's contract is visible at a glance in its constructor signature.
Best practices
- Mark dependencies
finaland inject through the constructor. - Group related collaborators into one focused service instead of injecting 10 things — that is a smell.
- Use
@Qualifierat the injection point, not@Primary, when the choice depends on the consumer.
Common mistakes
- Field injection makes circular dependencies easy to introduce and hard to spot.
- Injecting a prototype bean into a singleton without scope-proxy gives you the same instance forever.
- Calling
setPropertyfrom outside the container — Spring will not run validators or post-processors on values you set yourself.
Hands-on exercise
Refactor: take any class in your codebase that uses @Autowired on private fields and rewrite it with constructor injection and final fields. Write one unit test that constructs the class directly (no Spring). Notice how much easier the test is to read.