Implement dependency injection in Java with explicit constructor wiring first, then add containers only when the application boundary truly benefits.
Dependency injection: A design style where an object receives its collaborators from the outside instead of constructing or locating them itself.
The simplest useful version of dependency injection in Java does not start with Spring or Guice. It starts with constructor parameters.
1public final class InvoiceService {
2 private final TaxCalculator taxCalculator;
3 private final InvoiceRepository invoiceRepository;
4
5 public InvoiceService(TaxCalculator taxCalculator,
6 InvoiceRepository invoiceRepository) {
7 this.taxCalculator = taxCalculator;
8 this.invoiceRepository = invoiceRepository;
9 }
10}
This design is valuable because the dependency graph is visible:
The visual below shows the boundary:
flowchart LR
Bootstrap["Bootstrap / container"] --> Tax["TaxCalculator"]
Bootstrap --> Repo["InvoiceRepository"]
Tax --> Service["InvoiceService"]
Repo --> Service
If InvoiceService internally creates its own repository or calculator, it quietly takes ownership of configuration, lifecycle, and implementation choice. That makes the service harder to test and harder to adapt.
Dependency injection separates:
That separation is the real benefit, not the framework annotation.
Constructor injection is usually the best default when dependencies are required.
Setter injection can make sense for optional collaborators or post-construction configuration, but it weakens immutability and makes incomplete objects easier to create accidentally.
Field injection is convenient in frameworks, but it hides dependencies and makes plain construction harder. In codebases that care about test clarity and explicit design, it is usually the weakest option.
IoC containers become useful when object graphs are large enough that manual composition gets noisy. But the container should still reflect a clean design, not rescue a confused one.
A good DI-aware Java system still makes it easy to answer:
When reviewing dependency injection in Java, ask:
The best DI code is boring in the right way. It makes wiring visible and lets the business class focus on its job.