🍃 Section 5 · Question #2

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:

  1. 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 immediate NullPointerException because its collaborators were never wired.
  2. Inability to Use final (Immutability Violation): Fields injected reflectively cannot be declared final. This leaves dependencies mutable throughout the bean’s runtime lifecycle, creating thread-safety hazards and opening vulnerabilities to unauthorized reassignment.
  3. 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.
  4. Masking Single Responsibility Principle (SRP) Violations: Adding 10 dependencies via @Autowired fields 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 @Autowired field inside the class constructor throws a NullPointerException.

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 @PostConstruct method.

@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:

  1. After BeanFactory creates a raw instance via Constructor.newInstance(), it calls postProcessProperties().
  2. The post-processor queries the bean’s cached reflection metadata (InjectionMetadata), locating all fields and methods marked with @Autowired or @Value.
  3. It bypasses Java access boundaries using reflection:
    field.setAccessible(true); // Suppresses Java language access checks
    field.set(beanInstance, resolvedDependency);
    
  4. 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 final and 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 those final fields without requiring explicit memory barriers, locks, or volatile reads.
  • 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 volatile or synchronized).

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:

  1. Since Spring 4.3+, any bean declaring exactly one constructor does not require an explicit @Autowired annotation. Spring automatically treats that sole constructor as the autowiring target.
  2. Lombok compiles @RequiredArgsConstructor into an explicit constructor taking all three final parameters directly in bytecode.
  3. 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, @Autowired is 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 @Autowired to 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 with NoSuchMethodException.

4. Why is executing heavy business logic or network I/O in a Spring Bean’s constructor considered an antipattern?

Answer:

  1. Unwired Dependencies: In classes using field or setter injection, collaborators are still null during constructor execution.
  2. CGLIB Subclassing: Spring creates dynamic AOP proxies (@Transactional, @Async) by generating CGLIB subclasses at runtime. A CGLIB proxy’s constructor implicitly invokes super(). 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.
  3. Startup Blockage: Long-running operations in constructors block the Spring ApplicationContext initialization thread, drastically slowing down cold boots and health checks in containerized environments (Kubernetes). Heavy initialization should always be deferred to @PostConstruct or 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 final fields, 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, disables final, 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 @RequiredArgsConstructor eliminates boilerplate while keeping all constructor benefits).
  • ❌ “I access @Autowired fields inside my constructor to set up database connections.” (Fields are null inside the constructor; initialization must go into @PostConstruct or use constructor injection).
  • ❌ “You must put @Autowired on all constructors in Spring Boot 3.” (Single constructors do not require @Autowired since Spring 4.3).
  • ❌ “Field injection makes unit testing easier.” (It makes unit testing much harder because you cannot supply dependencies via standard Java new constructors).

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>