Constructor injection supplies required collaborators when an object is created, so the object does not need to locate them during an operation.
Spring constructor injection: required dependencies stay visible
This lesson uses the downloadable source kit: Java 21, Spring Boot 4.0.8 and its managed Spring Framework 7 dependencies. The version is pinned for repeatable builds.
Keep business code ordinary
ReceiptReport accepts Exporter in its constructor and stores it in a final field. The same class works when constructed directly in a unit test or created through the container. That is the useful boundary: choosing the implementation is assembly work, while rendering is business work.
The CoreBoundaryTest constructs the report without a context and verifies csv:receipt. Starting a web server to test that one method would add unrelated configuration, filters and serialization behavior. Small tests expose the dependency contract with less machinery.
Cycles signal an ownership problem
If service A requires B and B requires A to finish construction, there is no valid first complete object. Adding lazy lookup can defer the failure while preserving the design problem. Move the shared policy or orchestration into a third object when that matches the domain.
Injecting a Clock is the same pattern applied to time. A reservation service can ask its clock for the current instant rather than making hidden static reads. Expiry boundary tests then choose exact instants instead of waiting.
Checked source
public static class ReceiptReport {
private final Exporter exporter;
public ReceiptReport(Exporter exporter) { this.exporter = exporter; }
public String render() { return exporter.format() + ":receipt"; }
}Test the boundary
Run mvn test in the source-kit directory. CoreBoundaryTest checks the behavior described here. Java excerpts belong to the named source-kit classes; they are not independent source files unless the complete class is shown.
Costs and boundaries
The report keeps one collaborator reference; the exporter’s work determines operation cost. Dependency injection does not make an expensive exporter cheaper or add fault tolerance around it.
Common Mistakes
- Do not inject a global context to retrieve required business dependencies.
- Avoid constructor cycles rather than hiding them with lookup.
- Use direct construction when a test does not need the container.
Read next
Qualifiers, Testing boundaries, Java Clock: test expiry without sleeping.
Related boundary: Spring @Component versus @Bean: choose who constructs the dependency.
