Foundation · Understand
Dependency Injection Forms
Constructor, setter, and field injection differ in what they make impossible; constructor injection makes incomplete objects impossible.
Primary source: Spring Framework 6.2 Reference: Dependencies and Configuration in Detail
Lesson 7 of 14
1d streakThe Architectural Question
Field injection lets a class accumulate five dependencies quietly, hides them from tests, and allows partially constructed states that blow up at runtime.
What this guide proves
- Constructor Injection
- Field Injection
- Testability
The 3 injection forms: Constructor, setter, and field
Spring supports three primary mechanisms for injecting collaborators: Constructor Injection, Setter Injection, and Field Injection. Modern backend engineering standards universally mandate Constructor Injection for production services.
| Injection Type | Immutability (final) | Safe Publication (JMM) | Pure Unit Testing (No Spring) | Modern Recommendation |
|---|---|---|---|---|
| Constructor Injection | Supported (fields can be final) | Guaranteed by JMM final semantics | Effortless (new MyService(mock)) | Universal Standard (Required) |
| Setter Injection | Not supported (fields must be mutable) | Not guaranteed (thread visibility risk) | Manual setter calls required | Use only for optional reconfigurable dependencies |
| Field Injection (@Autowired on field) | Not supported (fields cannot be final) | Not guaranteed (reflection bypasses JMM) | Requires reflection or full Spring context | Forbidden Anti-Pattern |
@Service
public final class OrderService {
// 1. Immutable, safe, non-nullable fields
private final OrderRepository repository;
private final PaymentClient paymentClient;
// 2. Single constructor: Spring 4.3+ autowires automatically without @Autowired
public OrderService(OrderRepository repository, PaymentClient paymentClient) {
this.repository = Objects.requireNonNull(repository, "repository must not be null");
this.paymentClient = Objects.requireNonNull(paymentClient, "paymentClient must not be null");
}
}Constructor injection & the Java Memory Model (JMM)
Beyond clean code, constructor injection enforces fundamental Java Memory Model (JMM) safety guarantees. Under the JMM, fields marked final and initialized within a constructor are guaranteed to be safely published across threads without requiring synchronization or volatile memory barriers.
With setter or field injection, an object is first constructed with null fields, and the container populates those fields later. In concurrent multi-threaded environments, another thread can observe a "partially constructed" object whose reference is non-null but whose internal fields are still null.
Testing velocity without Spring Boot
Break it deliberately: The field injection NullPointerException trap
Field injection disguises the dependencies of a class, making it trivial for junior engineers to create classes with 15 hidden dependencies that collapse in unit tests.
// The Production Service with Field Injection
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // Hidden, private mutable field
public User getById(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
// In a fast Unit Test without Spring Boot Test runner:
@Test
void testGetById() {
UserService service = new UserService();
service.getById(42L); // Throws NullPointerException! userRepository was never set!
}java.lang.NullPointerException: Cannot invoke "com.example.UserRepository.findById(java.lang.Long)" because "this.userRepository" is null
at com.example.UserService.getById(UserService.java:14)
at com.example.FieldInjectionTest.testGetById(FieldInjectionTest.java:19)Why field injection is prohibited
Check your understanding
Active Recall Checkpoint
Why does constructor injection guarantee safe publication across threads under the Java Memory Model, whereas setter injection does not?
Hint: Think about final field semantics in the JMM specification.
Show answer & explanation
The Java Memory Model guarantees that fields marked final and initialized within the constructor are fully visible to all threads as soon as construction completes. Setter and field injection mutate fields post-construction, risking thread-visibility races where other threads observe partially-initialized null fields.
Active Recall Checkpoint
Since Spring 4.3, under what condition is the @Autowired annotation completely optional on a class constructor?
Hint: Consider how many constructors the class declares.
Show answer & explanation
If a class declares exactly one constructor, Spring automatically treats that constructor as the injection point, making @Autowired completely unnecessary.
Runtime Architecture & Flow
Decision canvas
Evaluate the choice across three constraints
These are comparison criteria, not sequential steps. A strong answer states which constraint dominates in the current system.
Constructor Injection
Ask how each option changes this guarantee, its operational cost, and its failure mode.
Field Injection
Ask how each option changes this guarantee, its operational cost, and its failure mode.
Testability
Ask how each option changes this guarantee, its operational cost, and its failure mode.
Decision prompt: Refactor a class using `@Autowired` on fields to use an all-arguments constructor (or a Java Record).
Working Implementation
Code Walkthrough
Refactor a class using `@Autowired` on fields to use an all-arguments constructor (or a Java Record).
package com.example.core;
import org.springframework.stereotype.Service;
public class ConstructorVsFieldLab {
public interface OrderRepository {
void persist(String orderId);
}
// PRODUCTION BEST PRACTICE: Immutable constructor injection
@Service
public static final class OrderService {
private final OrderRepository repository;
// Single constructor: @Autowired is implicit since Spring 4.3
public OrderService(OrderRepository repository) {
if (repository == null) {
throw new IllegalArgumentException("repository collaborator must not be null");
}
this.repository = repository;
}
public void process(String orderId) {
repository.persist(orderId);
}
}
public static void main(String[] args) {
// 1. Plain unit testing without Spring: instant execution, zero reflection
OrderRepository stubRepo = id -> System.out.println("Stored order: " + id);
var service = new OrderService(stubRepo);
service.process("ORD-9021");
// 2. Immutability guaranteed: compiler refuses half-constructed state
try {
new OrderService(null);
} catch (IllegalArgumentException ex) {
System.out.println("Fail-fast prevented incomplete object: " + ex.getMessage());
}
}
}Synthesis & Production Verification
Production Acceptance Criteria
- Refactor a field-injected class to constructor injection
- Instantiate it in a plain unit test without starting Spring
- Explain why Spring omits @Autowired on single-constructor classes
Key Takeaway
Constructor injection enforces immutability, thread-safety under the Java Memory Model, and prevents partially-constructed objects while making unit testing fast and framework-independent.
Architectural & Technical Review
Core Interview Questions
Q01Why does the Spring team mandate constructor injection over field injection for production backend services?Show answer
Constructor injection ensures required dependencies cannot be null, allows fields to be final and immutable, guarantees safe publication under the JMM, and allows unit tests to instantiate services without Spring or reflection.
Q02How does constructor injection guarantee thread safety under the Java Memory Model (JMM)?Show answer
The JMM guarantees that final fields initialized in a constructor are completely visible to all threads upon constructor completion without synchronization. Mutable fields populated later via reflection risk thread-visibility races.
Q03What is the primary operational defect caused by field injection in test suites?Show answer
Field-injected services cannot be instantiated in plain unit tests with new MyService(mockRepo), forcing tests to either boot the entire Spring context (slow) or use brittle reflection to set private fields.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Using field injection (@Autowired on private fields) — Field injection hides dependencies from callers, prevents final immutability, and causes NullPointerExceptions in plain JUnit unit tests.
- Pitfall: Accumulating excessive constructor parameters — More than 4-5 constructor dependencies is an architectural smell indicating that a class is violating the Single Responsibility Principle and should be refactored.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources