Ch. 8 · Spring Boot

Spring Constructor Injection and Required Dependencies

Why Spring recommends constructor injection over @Autowired fields: final dependencies, plain unit tests, fail-fast startup and visible cycles.

~6 min readbeginnerupdated Oct 6, 2026

“Field injection, setter injection or constructor injection: which do you use and why?” Every Spring interviewer asks a version of this. Saying “constructor injection, because the Spring team recommends it” is correct but thin. The interviewer wants the reasons: immutability, required dependencies that cannot be forgotten, plain unit tests without a container, and design pressure that exposes classes doing too much.

Before you start

You should know what a Spring bean is, what @Service and @Component do, and how to write a JUnit 5 test with Mockito. Examples target Spring Boot 3.x on Java 17+.

The short answer

Use constructor injection for required dependencies. Declare them as private final fields and assign them in the single constructor; Spring has used a lone constructor automatically since 4.3, so no @Autowired is needed. The object is fully initialized the moment it exists, it can be built with new in a unit test, missing beans fail startup with a clear message, and circular dependencies are reported instead of silently wired. Use setter injection only for genuinely optional dependencies, and avoid field injection in production code.

How it works

When Spring creates a bean, it picks a constructor. With exactly one constructor, Spring uses it. With several, it uses the one annotated @Autowired, or the no-argument constructor if present. For each parameter, Spring resolves a bean by type (then by qualifier or parameter name if there are several candidates) and calls the constructor once with every argument ready.

@Service
public class CheckoutService {

    private final CartRepository carts;
    private final PaymentGateway payments;
    private final Clock clock;

    public CheckoutService(CartRepository carts, PaymentGateway payments, Clock clock) {
        this.carts = carts;
        this.payments = payments;
        this.clock = clock;
    }

    public Receipt checkout(long cartId) {
        Cart cart = carts.findById(cartId).orElseThrow();
        Charge charge = payments.charge(cart.total());
        return new Receipt(cart.id(), charge.id(), clock.instant());
    }
}
java

Compare field injection:

@Service
public class CheckoutService {
    @Autowired private CartRepository carts;
    @Autowired private PaymentGateway payments;
}
java

Here Spring calls the no-argument constructor, then uses reflection to set private fields. Between those two moments, the object exists with null dependencies. The fields cannot be final. Nothing outside Spring can set them without reflection.

Step-by-step walkthrough

Step 1: Convert field injection to a constructor

Make each field private final, delete @Autowired, and add a constructor. If the project uses Lombok, @RequiredArgsConstructor generates the constructor for final fields.

@Service
@RequiredArgsConstructor
public class CheckoutService {
    private final CartRepository carts;
    private final PaymentGateway payments;
    private final Clock clock;
}
java

The compiler now enforces that every dependency is assigned exactly once.

Step 2: Write a plain unit test

No @SpringBootTest, no @InjectMocks reflection: just construct the class.

class CheckoutServiceTest {

    CartRepository carts = mock(CartRepository.class);
    PaymentGateway payments = mock(PaymentGateway.class);
    Clock clock = Clock.fixed(Instant.parse("2026-10-06T10:00:00Z"), ZoneOffset.UTC);

    CheckoutService service = new CheckoutService(carts, payments, clock);

    @Test
    void stampsReceiptWithClockTime() {
        when(carts.findById(1L)).thenReturn(Optional.of(new Cart(1L, new BigDecimal("20.00"))));
        when(payments.charge(any())).thenReturn(new Charge("ch_1"));

        Receipt receipt = service.checkout(1L);

        assertThat(receipt.createdAt()).isEqualTo(Instant.parse("2026-10-06T10:00:00Z"));
    }
}
java

If you add a fourth dependency, this test stops compiling. With field injection it would compile and fail with a NullPointerException at runtime.

Step 3: Handle optional dependencies explicitly

When a dependency might not exist (an optional metrics exporter, say), say so in the signature instead of leaving a nullable field:

public AuditService(AuditRepository repo, ObjectProvider<AuditPublisher> publisher) {
    this.repo = repo;
    this.publisher = publisher;   // call publisher.ifAvailable(p -> p.publish(event))
}
java

Optional<AuditPublisher> also works as a parameter type.

Step 4: Listen to the constructor

A constructor with nine parameters is a signal that the class has too many responsibilities. Field injection hides this because nine @Autowired lines look harmless. Split the class along the responsibilities the parameters reveal.

Worked scenario

A developer writes a new helper with field injection and creates it manually inside a service:

@Component
public class DiscountCalculator {
    @Autowired private PricingRules rules;

    public BigDecimal discount(Order order) {
        return rules.forCustomer(order.customerId()).apply(order.total());
    }
}

@Service
public class OrderService {
    public Order place(Order order) {
        BigDecimal discount = new DiscountCalculator().discount(order);   // NPE: rules is null
        return order.withDiscount(discount);
    }
}
java

The NullPointerException happens because new bypasses Spring entirely, so the @Autowired field is never set. This is exactly the pattern covered in why an @Autowired field is null.

With constructor injection, the bug cannot be written. new DiscountCalculator() does not compile, because the only constructor requires PricingRules. The developer is pushed to inject the calculator:

@Component
public class DiscountCalculator {
    private final PricingRules rules;
    public DiscountCalculator(PricingRules rules) { this.rules = rules; }
    public BigDecimal discount(Order order) { /* same logic */ return BigDecimal.ZERO; }
}

@Service
public class OrderService {
    private final DiscountCalculator calculator;
    public OrderService(DiscountCalculator calculator) { this.calculator = calculator; }
}
java

Common mistake

  • “You need @Autowired on the constructor.” Not with a single constructor. You need it only to choose among several.
  • “Constructor injection causes circular dependency errors, so field injection is better.” Constructor injection reveals the cycle; field injection used to hide it. Since Spring Boot 2.6, circular references are prohibited by default regardless. Fix the design, as described in circular dependency fixes.
  • Mixing final fields with field injection. @Autowired private final Foo foo; does not compile without a constructor.
  • Using @InjectMocks everywhere. It silently falls back between constructor, setter and field injection, and silently leaves fields null when it cannot match. A real constructor call is clearer.
  • Calling overridable methods or dependencies from a constructor in ways that assume proxies or @PostConstruct work has already happened. Keep constructors to assignments.

Verify the behavior

Prove that a missing dependency fails startup instead of producing a null field later. ApplicationContextRunner makes this a fast test:

class CheckoutWiringTest {

    @Test
    void failsFastWhenPaymentGatewayIsMissing() {
        new ApplicationContextRunner()
            .withBean(CartRepository.class, () -> mock(CartRepository.class))
            .withBean(Clock.class, Clock::systemUTC)
            .withBean(CheckoutService.class)
            .run(ctx -> assertThat(ctx).hasFailed()
                .getFailure().rootCause().hasMessageContaining("PaymentGateway"));
    }
}
java

The root cause is a NoSuchBeanDefinitionException naming PaymentGateway, the same information Boot prints in its “required a bean of type … that could not be found” report.

Follow-up questions

When is setter injection appropriate? For optional dependencies with sensible defaults, or for reconfigurable collaborators in legacy code. It is rare in modern Boot apps.

How does Spring choose between two beans of the same type for a constructor parameter? @Primary, then @Qualifier, then the parameter name matched to a bean name (which requires compiling with -parameters, as Boot’s build plugins do). Otherwise it fails; see no qualifying bean.

Does constructor injection work with @Value? Yes: MyService(@Value("${app.name}") String name). For groups of settings, prefer a @ConfigurationProperties record.

Is field injection ever acceptable? In test classes, @Autowired fields are common and harmless because the test framework owns the lifecycle.

Interview exercise

A class has eight @Autowired fields. Convert it to constructor injection. The resulting constructor has eight parameters, and a teammate says this proves constructor injection is worse. How do you respond?

Answer and reasoning

The eight-parameter constructor is not a new problem; it is the existing problem made visible. Field injection hid that the class coordinates eight collaborators. Respond by grouping the parameters by what they serve: if three are about pricing and three about notifications, extract a PricingService and a NotificationService, leaving the original class with a short constructor that orchestrates them. Each extracted class gets its own focused tests built with new. The reasoning interviewers want: constructor injection gives immutability and testability, and its “verbosity” is design feedback rather than a flaw.

Continue learning

More in Spring Boot

read ✓Spring Boot · hard

Spring Boot Testcontainers

Run integration tests against real Postgres in Docker with @ServiceConnection, share one container across test classes, and keep tests isolated.

~6 min readread →
esc