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

20 min

Lesson 7 of 14

1d streak

The 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 TypeImmutability (final)Safe Publication (JMM)Pure Unit Testing (No Spring)Modern Recommendation
Constructor InjectionSupported (fields can be final)Guaranteed by JMM final semanticsEffortless (new MyService(mock))Universal Standard (Required)
Setter InjectionNot supported (fields must be mutable)Not guaranteed (thread visibility risk)Manual setter calls requiredUse 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 contextForbidden Anti-Pattern
OrderService.java
@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

Constructor-injected services can be unit-tested in 2 milliseconds with plain JUnit and Mockito: new OrderService(mockRepo, mockClient). Field-injected services require booting the Spring container or using brittle reflection, slowing test suites from seconds to minutes.

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.

FieldInjectionTest.java
// 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!
}
Terminal / Diagnostic Output
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

Field injection hides requirements from callers, prevents final immutability, breaks plain Java unit tests, and bypasses Java compiler checking. Always use constructor injection.

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.

CRITERION 01

Constructor Injection

Ask how each option changes this guarantee, its operational cost, and its failure mode.

CRITERION 02

Field Injection

Ask how each option changes this guarantee, its operational cost, and its failure mode.

CRITERION 03

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).

ConstructorVsFieldLab.java
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());
        }
    }
}
Constructor injection guarantees all required dependencies are final and non-null before any method executes.
Field injection hides dependencies, allows incomplete construction states, and requires reflection or Spring test runners to mock.
Single-constructor classes do not require @Autowired; Spring discovers and uses the constructor automatically.

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

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

Supplementary Module Resources