Difference between constructor, setter and field injection
In the Spring Framework, dependencies can be wired into a bean using three mechanisms: Constructor Injection, Setter Injection, and Field Injection.
🟢 Junior Level
1. The Three Dependency Injection Strategies
In the Spring Framework, dependencies can be wired into a bean using three mechanisms: Constructor Injection, Setter Injection, and Field Injection.
// 1. Constructor Injection (Recommended Production Standard)
@Service
public class OrderService {
private final OrderRepository repository;
// In Spring 4.3+, @Autowired is optional if there is only one constructor
public OrderService(OrderRepository repository) {
this.repository = repository;
}
}
// 2. Setter Injection (Used for Optional Dependencies)
@Service
public class OrderService {
private NotificationService notifier;
@Autowired(required = false)
public void setNotifier(NotificationService notifier) {
this.notifier = notifier;
}
}
// 3. Field Injection (Anti-Pattern — Avoid in Production)
@Service
public class OrderService {
@Autowired
private OrderRepository repository; // Injected reflectively into private field
}
2. Feature Comparison Matrix
| Dimension | Constructor Injection | Setter Injection | Field Injection |
|---|---|---|---|
Immutability (final) |
✅ Guaranteed (final fields) |
❌ No (Mutable fields) | ❌ No (Cannot declare final) |
| Null Safety | ✅ 100% (No partially initialized states) | ⚠️ Risky (Setters may be skipped) | ❌ High risk (Fields are null in plain constructors) |
| POJO Unit Testing | ✅ Effortless (new Service(mock)) |
⚠️ Moderate (Must invoke setters) | ❌ High friction (Requires reflection / @InjectMocks) |
| Contract Explicitness | ✅ Dependencies clear in signature | ⚠️ Scattered across methods | ❌ Hidden from public API |
| Circular Dependencies | Fails fast at startup (BeanCurrentlyInCreationException) |
Masked via 3-level cache | Masked via 3-level cache |
| Industry Status | 🟢 Mandatory Standard (99%) | 🟡 Optional / Reconfigurable only | 🔴 Anti-Pattern (Banned) |
🟡 Middle Level
1. The Four Fatal Flaws of Field Injection
Despite its visual brevity, Field Injection (@Autowired directly on private fields) is an acknowledged architectural anti-pattern for four major reasons:
- Encapsulation Violation & Hidden Dependencies:
A class with Field Injection exposes a default no-argument constructor
new OrderService(). A developer instantiating the class in a unit test or outside Spring expects a functional service. However, invoking any business method triggers an immediateNullPointerExceptionbecause its collaborators were never wired. - Inability to Use
final(Immutability Violation): Fields injected reflectively cannot be declaredfinal. This leaves dependencies mutable throughout the bean’s runtime lifecycle, creating thread-safety hazards and opening vulnerabilities to unauthorized reassignment. - Severe Unit Testing Friction:
To test a class using Field Injection without starting a full, heavy Spring
ApplicationContext, developers are forced to use reflection hacks (ReflectionTestUtils.setField(...)) or rely on Mockito’s@InjectMocks. If a field name changes, tests break silently at runtime. - Masking Single Responsibility Principle (SRP) Violations:
Adding 10 dependencies via
@Autowiredfields takes 10 compact lines and visually masks code bloat. In contrast, a constructor requiring 10 arguments creates an immediate, unmistakable code smell that prompts developers to refactor and decompose the class.
2. Chronological Bean Lifecycle Order
A frequent source of bugs is misunderstanding the sequence in which Spring constructs and populates beans:
[ 1. Constructor Invocation ] ──► Constructor Injection resolved here
│
▼
[ 2. Raw Instance Created in Heap ]
│
▼
[ 3. Field Injection ] ──► @Autowired fields populated via reflection
│
▼
[ 4. Setter Injection ] ──► @Autowired setters invoked
│
▼
[ 5. Initialization ] ──► @PostConstruct / InitializingBean.afterPropertiesSet()
[!WARNING] The Constructor Field Access Trap: Calling a method on an
@Autowiredfield inside the class constructor throws aNullPointerException.When the constructor executes, the object is only being allocated; field injection has not yet occurred! If you need initialization logic that depends on injected fields, use Constructor Injection or move the logic into a
@PostConstructmethod.
@Service
public class MetricReporter {
@Autowired
private MetricClient metricClient;
public MetricReporter() {
// ❌ BUG: metricClient is NULL here!
metricClient.connect(); // Throws NullPointerException!
}
@PostConstruct
public void init() {
// ✅ CORRECT: Field injection has completed before @PostConstruct runs
metricClient.connect();
}
}
🔴 Senior Level
1. Under the Hood: AutowiredAnnotationBeanPostProcessor
Field and Setter injection are not performed by the Java compiler or standard classloading; they are driven by Spring’s AutowiredAnnotationBeanPostProcessor:
- After
BeanFactorycreates a raw instance viaConstructor.newInstance(), it callspostProcessProperties(). - The post-processor queries the bean’s cached reflection metadata (
InjectionMetadata), locating all fields and methods marked with@Autowiredor@Value. - It bypasses Java access boundaries using reflection:
field.setAccessible(true); // Suppresses Java language access checks field.set(beanInstance, resolvedDependency); - This incurs runtime reflection overhead during application startup and violates Java modularity encapsulation (especially in modern Java 17+ with the JPMS module system).
Contrast with Constructor Injection:
During Constructor Injection, Spring’s ConstructorResolver determines candidate constructors at the Instantiation Phase. It queries DefaultListableBeanFactory.resolveDependency() for each parameter and passes the resolved instances directly into Constructor.newInstance(resolvedArgs). No private field access bypass occurs.
2. Java Memory Model (JMM) Safe Publication Guarantees
Under the Java Memory Model (§17.5 Final Field Semantics):
- When a field is declared
finaland initialized inside a constructor, the JMM guarantees that any thread obtaining a reference to that object will observe the fully constructed, up-to-date values of thosefinalfields without requiring explicit memory barriers, locks, orvolatilereads. - This is known as Safe Publication.
In contrast:
- Field and Setter injection leave fields non-final.
- If a bean reference escapes prematurely or is shared across concurrent worker threads, there is no formal JMM guarantee that other CPU cores will see the updated dependency references immediately without a memory synchronization barrier (such as
volatileorsynchronized).
3. Idiomatic Production Architecture: Constructor Injection + Lombok
To eliminate constructor boilerplate in enterprise applications, modern production code uses Lombok’s @RequiredArgsConstructor:
@Service
@RequiredArgsConstructor
public class AccountService {
// Lombok generates a constructor containing all `final` fields:
private final AccountRepository accountRepository;
private final NotificationService notificationService;
private final AuditLogger auditLogger;
// No @Autowired or manual constructor required!
}
Why This Works:
- Since Spring 4.3+, any bean declaring exactly one constructor does not require an explicit
@Autowiredannotation. Spring automatically treats that sole constructor as the autowiring target. - Lombok compiles
@RequiredArgsConstructorinto an explicit constructor taking all threefinalparameters directly in bytecode. - You get immutability, safe publication, compile-time safety, and effortless unit testing with zero boilerplate.
4 Tricky Questions
1. Why does calling an @Autowired field inside the constructor throw NullPointerException, and how does @PostConstruct resolve this?
Answer:
A class constructor’s sole responsibility is allocating and constructing the raw object instance in heap memory. In Spring’s lifecycle, dependency injection for fields occurs in the Property Population Phase, which happens strictly after the constructor has returned. Therefore, during constructor execution, all fields marked with @Autowired still hold their default JVM values (null).
The @PostConstruct annotation belongs to the Initialization Phase, which executes after all dependencies (constructor, field, and setter) have been fully wired by AutowiredAnnotationBeanPostProcessor. By the time @PostConstruct methods run, all fields are guaranteed to be non-null and ready for use.
2. What is the Java Memory Model (JMM) advantage of Constructor Injection over Field Injection?
Answer:
Constructor Injection allows dependencies to be assigned to final fields. Under JMM Specification §17.5 (“Final Field Semantics”), the JVM enforces a freeze action on final fields at the end of the constructor. This ensures Safe Publication: all threads are mathematically guaranteed to see the properly initialized dependency references without data races, caching anomalies, or the need for volatile synchronization.
With Field Injection, fields are mutable (non-final) and populated post-instantiation via reflection. Under concurrent access, CPU cache incoherency could theoretically allow another thread to read an uninitialized null or stale reference if safe publication is not explicitly enforced.
3. Is @Autowired required on constructors in Spring 4.3+ and Spring 6+, and what happens if a class defines two constructors?
Answer:
- Single Constructor: If a bean declares exactly one constructor,
@Autowiredis completely optional. Spring automatically treats that constructor as the autowiring target. - Multiple Constructors: If a bean declares two or more constructors, Spring cannot determine which constructor to use by default. You must explicitly annotate one constructor with
@Autowiredto instruct the container which constructor to invoke. - Exception: If no constructor has
@Autowired, but one of the constructors is a public no-arg constructor, Spring falls back to using the default no-arg constructor and populates the bean via field/setter injection. If no default constructor exists and none has@Autowired, the container crashes at startup withNoSuchMethodException.
4. Why is executing heavy business logic or network I/O in a Spring Bean’s constructor considered an antipattern?
Answer:
- Unwired Dependencies: In classes using field or setter injection, collaborators are still
nullduring constructor execution. - CGLIB Subclassing: Spring creates dynamic AOP proxies (
@Transactional,@Async) by generating CGLIB subclasses at runtime. A CGLIB proxy’s constructor implicitly invokessuper(). If heavy logic or network I/O resides in the constructor, it may be executed multiple times—once for the target bean and again when the proxy subclass is instantiated. - Startup Blockage: Long-running operations in constructors block the Spring
ApplicationContextinitialization thread, drastically slowing down cold boots and health checks in containerized environments (Kubernetes). Heavy initialization should always be deferred to@PostConstructor background event listeners (ApplicationReadyEvent).
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“Spring supports three injection types: Constructor, Setter, and Field Injection. Constructor Injection is the undisputed industry standard: it enforces immutability via
finalfields, guarantees fully initialized objects (preventing NPEs), provides JMM safe publication, allows pure POJO unit testing without Spring, and fails fast on circular dependencies. Setter Injection should be reserved strictly for optional dependencies using@Autowired(required = false). Field Injection is an anti-pattern that violates encapsulation, hides class dependencies, disablesfinal, requires reflection in tests, and masks SRP violations.”
Red Flags & Pitfalls (DO NOT Say)
- ❌ “Field injection is better because it writes fewer lines of boilerplate code.” (Lombok
@RequiredArgsConstructoreliminates boilerplate while keeping all constructor benefits). - ❌ “I access
@Autowiredfields inside my constructor to set up database connections.” (Fields arenullinside the constructor; initialization must go into@PostConstructor use constructor injection). - ❌ “You must put
@Autowiredon all constructors in Spring Boot 3.” (Single constructors do not require@Autowiredsince Spring 4.3). - ❌ “Field injection makes unit testing easier.” (It makes unit testing much harder because you cannot supply dependencies via standard Java
newconstructors).
Injection Strategy Decision Guide
Dependency Injection Decision Flow:
├─ Is the dependency required for the bean to function?
│ └─ YES ──> Use Constructor Injection + final fields
│ └─ Boilerplate concern? ──> Use Lombok @RequiredArgsConstructor
└─ Is the dependency optional, or does it have a reasonable default?
└─ YES ──> Use Setter Injection with @Autowired(required = false)
└─ Or Constructor Injection with Optional<Dependency>