Back to Dependency Injection
Q2Question 2 of 3

What is constructor vs field vs setter injection?

Key Concepts
  • Dependency Injection is the mechanism where the Spring container provides a bean's dependencies from outside rather than the bean creating them itself.
  • Constructor injection is preferred because it gives immutability (final fields), completeness (bean cannot exist without all deps), compile-time safety, and easy testability.
  • Field injection with @Autowired is the most common anti-pattern because dependencies are hidden, fields cannot be final, and testing requires a Spring context or reflection.
  • Setter injection is for truly optional dependencies using @Autowired(required = false).
  • Since Spring 4.3, a single constructor does not need @Autowired because Spring auto-detects it.
  • Lombok @RequiredArgsConstructor generates the constructor for all final fields, giving zero-boilerplate constructor injection.
  • A many-parameter constructor is a signal that the class has too many responsibilities and should be split.

Definition

Constructor injection with final fields is preferred because it provides immutability, completeness, compile-time safety, and straightforward testability; Lombok @RequiredArgsConstructor eliminates the boilerplate.
  • Dependency Injection (DI) -- the mechanism where the Spring container provides dependencies from outside the bean rather than the bean creating them with new.
  • Constructor injection -- dependencies passed as constructor parameters and stored in final fields; the bean cannot be created without all dependencies.
  • Field injection -- @Autowired placed on a private field; convenient but hides dependencies, prevents final, and makes testing hard.
  • Setter injection -- @Autowired on a setter method; used for truly optional dependencies.
  • @RequiredArgsConstructor -- Lombok annotation that generates a constructor for all final fields, providing zero-boilerplate constructor injection.

Constructor injection is like the branch manager at National Bank of India handing Rajesh ALL his equipment when he arrives at his desk. Nothing is missing, and nothing can be swapped later. Field injection is like equipment appearing in Rajesh's drawer overnight by magic: he cannot see what is there until he opens it, and testing requires the magic manager to place items. Setter injection is equipment delivered later, maybe, optionally.

Understanding the Concepts

What is Dependency Injection?

Dependency Injection is the mechanism where the Spring container provides a bean's dependencies from outside rather than the bean creating them itself with the new keyword. The bean declares what it needs, and the container fulfills those needs at creation time.

There are three injection styles: constructor, field, and setter. The Spring team officially recommends constructor injection for required dependencies.

How does constructor injection work?

With constructor injection, dependencies are passed as constructor parameters and stored in final fields. The bean cannot be created without all its dependencies, and those dependencies cannot be changed after construction.

Since Spring 4.3, a single constructor does not need @Autowired because Spring auto-detects it.

JAVA
package com.bank.service;

import com.bank.repository.AccountRepository;
import com.bank.repository.AuditRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
public class LoanApprovalService {
    private final AccountRepository accountRepo;
    private final CreditScoreService creditService;
    private final AuditRepository auditRepo;

    // Single constructor: no @Autowired needed (Spring 4.3+)
    public LoanApprovalService(
            AccountRepository accountRepo,
            CreditScoreService creditService,
            AuditRepository auditRepo) {
        this.accountRepo = accountRepo;
        this.creditService = creditService;
        this.auditRepo = auditRepo;
    }

    @Transactional
    public boolean approveLoan(String accountId, BigDecimal amount) {
        var account = accountRepo.findById(accountId).orElseThrow();
        int score = creditService.getScore(accountId);
        boolean approved = score >= 700 && amount.compareTo(new BigDecimal("1000000")) <= 0;
        auditRepo.logDecision(accountId, amount, approved);
        return approved;
    }
}
TEXT
Loan for SAV2001: amount=500000, score=750, approved=true

How does field injection work and why should you avoid it?

Field injection uses @Autowired on a private field. The container uses reflection to set the field after construction. Dependencies are hidden from the constructor signature, cannot be final, and testing requires either a Spring context or Mockito's @InjectMocks with reflection.

JAVA
package com.bank.service;

import com.bank.repository.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class LoanApprovalService {
    @Autowired
    private AccountRepository accountRepo; // not final, hidden

    @Autowired
    private CreditScoreService creditService; // not final, hidden

    @Autowired
    private AuditRepository auditRepo; // not final, hidden

    // No constructor: dependencies invisible to callers
}

Problems with field injection:

  • Hidden dependencies. You cannot tell what the class needs without reading the source.
  • No immutability. Fields cannot be final because they are set after construction.
  • No compile-time safety. A missing dependency compiles fine and fails at runtime.
  • Hard to test. You need @SpringBootTest or reflection-based @InjectMocks.

How does setter injection work?

Setter injection uses @Autowired on a setter method. Use it only for truly optional dependencies that the bean can function without.

JAVA
package com.bank.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class TransferService {
    private final AccountRepository accountRepo;
    private NotificationService notificationService; // optional

    public TransferService(AccountRepository accountRepo) {
        this.accountRepo = accountRepo; // required via constructor
    }

    @Autowired(required = false) // optional: works without notification
    public void setNotificationService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void transfer(String from, String to, java.math.BigDecimal amount) {
        // ... transfer logic
        if (notificationService != null) {
            notificationService.send(to, "Received " + amount);
        }
    }
}

How is constructor injection different from field and setter injection?

| Aspect | Constructor | Field | Setter |
|---|---|---|---|
| Immutability | final fields | Not possible | Not possible |
| Completeness | Cannot create without all deps | Can create with null fields | Can create without optional deps |
| Visibility | Deps in constructor signature | Hidden in private fields | In setter methods |
| Compile safety | Missing dep = compile error | Compiles, fails at runtime | Compiles, fails at runtime |
| Testability | new Service(mock1, mock2) | Needs Spring or reflection | Call setter with mock |
| @Autowired needed | No (single constructor, Spring 4.3+) | Yes | Yes |
| Circular deps | Fails immediately | Resolved by 3-level cache | Resolved by 3-level cache |
| Lombok support | @RequiredArgsConstructor | N/A | N/A |

JAVA
package com.bank.service;

import com.bank.model.Account;
import com.bank.repository.AccountRepository;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;

// Constructor injection: simple unit test, no Spring context
class LoanApprovalServiceTest {
    @Test
    void shouldRejectLowCreditScore() {
        AccountRepository accountRepo = mock(AccountRepository.class);
        CreditScoreService creditService = mock(CreditScoreService.class);
        AuditRepository auditRepo = mock(AuditRepository.class);

        when(accountRepo.findById("SAV2001"))
            .thenReturn(java.util.Optional.of(new Account("SAV2001")));
        when(creditService.getScore("SAV2001")).thenReturn(550);

        // Create with new + mocks: 50ms, no Spring context
        var service = new LoanApprovalService(accountRepo, creditService, auditRepo);
        boolean result = service.approveLoan("SAV2001", new BigDecimal("500000"));

        assertThat(result).isFalse();
        verify(auditRepo).logDecision("SAV2001", new BigDecimal("500000"), false);
    }
}
TEXT
LoanApprovalServiceTest > shouldRejectLowCreditScore PASSED (48ms)

How does Lombok @RequiredArgsConstructor work?

Lombok's @RequiredArgsConstructor generates a constructor for all final fields at compile time. This eliminates constructor boilerplate while keeping all the benefits of constructor injection.

JAVA
package com.bank.service;

import com.bank.repository.AccountRepository;
import com.bank.repository.AuditRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
@RequiredArgsConstructor // generates constructor for all final fields
public class LoanApprovalService {
    private final AccountRepository accountRepo;
    private final CreditScoreService creditService;
    private final AuditRepository auditRepo;

    // No constructor written -- Lombok generates:
    // public LoanApprovalService(AccountRepository accountRepo,
    //     CreditScoreService creditService, AuditRepository auditRepo) {
    //     this.accountRepo = accountRepo;
    //     this.creditService = creditService;
    //     this.auditRepo = auditRepo;
    // }

    @Transactional
    public boolean approveLoan(String accountId, BigDecimal amount) {
        var account = accountRepo.findById(accountId).orElseThrow();
        int score = creditService.getScore(accountId);
        return score >= 700 && amount.compareTo(new BigDecimal("1000000")) <= 0;
    }
}

Spring detects the generated single constructor and injects all three dependencies without any @Autowired annotation. This is the idiomatic modern Spring Boot style.

What are common mistakes with DI?

WRONG -- field injection making tests slow:

JAVA
package com.bank.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountService {
    @Autowired
    private AccountRepository repo; // field injection

    @Autowired
    private AuditService audit; // field injection
}
JAVA
package com.bank.service;

import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;

@SpringBootTest // loads ENTIRE Spring context -- slow!
class AccountServiceTest {
    @Autowired
    private AccountService accountService;

    @Test
    void test() {
        // Takes 3-8 seconds to start Spring context
    }
}

CORRECT -- constructor injection with fast unit tests:

JAVA
package com.bank.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AccountService {
    private final AccountRepository repo;
    private final AuditService audit;
}
JAVA
package com.bank.service;

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

class AccountServiceTest {
    @Test
    void test() {
        // Plain Mockito -- 50ms, no Spring context
        var service = new AccountService(
            mock(AccountRepository.class),
            mock(AuditService.class)
        );
    }
}
TEXT
Field injection tests: 3200ms (loading Spring context)
Constructor injection tests: 48ms (plain Mockito)

WRONG -- too many constructor parameters (SRP violation):

JAVA
@Service
@RequiredArgsConstructor
public class LoanService {
    private final AccountRepository accountRepo;
    private final CreditScoreService creditService;
    private final RiskAssessmentService riskService;
    private final ComplianceService complianceService;
    private final NotificationService notificationService;
    private final AuditService auditService;
    private final DocumentService documentService;
    // 7 dependencies -- this class does too much!
}

A constructor with more than 5 parameters signals that the class has too many responsibilities. Extract related dependencies into a dedicated service.

What should a senior engineer know?

  • Constructor injection reveals design problems. A growing parameter list makes it obvious that the class has too many responsibilities. Field injection hides this because you just add another @Autowired field.
  • Lombok + constructor injection is the idiomatic style in modern Spring Boot applications. The Spring team recommends this combination.
  • Multiple constructors. If a class has multiple constructors, annotate the one Spring should use with @Autowired. Without the annotation, Spring picks the default no-arg constructor.
  • Record classes (Java 16+). Records have an implicit canonical constructor, making them natural for constructor injection in configuration holders, though they are less common for service beans.
  • Spring Boot 3 + AOT. Ahead-of-time compilation works best with constructor injection because the constructor parameters are explicit. Field injection requires reflection, which GraalVM native images handle less efficiently.

What are common interview follow-up questions?

  • Why does the Spring team recommend constructor injection? (Immutability, testability, compile safety.)
  • Can you use @Autowired on a constructor with multiple parameters? (Yes, each parameter is resolved independently.)
  • What happens if you have two constructors and no @Autowired? (Spring uses the no-arg constructor if available, or fails if it cannot decide.)
  • How do you inject an optional dependency? (Setter with @Autowired(required = false) or ObjectProvider.)
  • Is @RequiredArgsConstructor from Spring? (No, it is from Lombok. Spring just detects the generated constructor.)

Visual Overview

DIAGRAM
  Constructor Injection:
  ┌────────────────────────────────────────┐
  │  Container creates bean:               │
  │                                        │
  │  new LoanApprovalService(              │
  │      accountRepo,    ← from container  │
  │      creditService,  ← from container  │
  │      auditRepo       ← from container  │
  │  )                                     │
  │       │                                │
  │       ▼                                │
  │  All fields are final, bean is ready   │
  └────────────────────────────────────────┘

  Field Injection:
  ┌────────────────────────────────────────┐
  │  Container creates bean:               │
  │                                        │
  │  new LoanApprovalService()  ← empty    │
  │       │                                │
  │       ▼  (reflection)                  │
  │  field.set(accountRepo)                │
  │  field.set(creditService)              │
  │  field.set(auditRepo)                  │
  │       │                                │
  │       ▼                                │
  │  Fields not final, bean is ready       │
  └────────────────────────────────────────┘

Key Takeaway

Dependency Injection is the core mechanism of Spring's IoC container, and there are three styles: constructor, field, and setter. Constructor injection is preferred because it provides immutability through final fields, completeness because the bean cannot be created without all dependencies, compile-time safety because the compiler catches missing dependencies, and straightforward testability because you create the bean with new and pass mocks directly. Field injection with @Autowired is the most common anti-pattern in Spring applications because dependencies are hidden, fields cannot be final, and testing requires either a full Spring context or Mockito's reflection-based @InjectMocks.

In Rajesh's loan approval service at National Bank of India, four dependencies (AccountRepository, CreditScoreService, RiskAssessmentService, and AuditRepository) are declared as final fields with Lombok @RequiredArgsConstructor generating the constructor automatically. Spring detects the single constructor and injects all four dependencies without any @Autowired annotation. In unit tests, Mockito creates the service with four mock arguments in 48 milliseconds instead of 3200 milliseconds with @SpringBootTest. When Rajesh added a fifth dependency for compliance checking, the compiler immediately flagged every test missing the new mock, catching the error at compile time.

The migration from field injection to constructor injection also made design problems visible. A five-parameter constructor prompted Pankaj to extract credit scoring and risk assessment into a dedicated LoanEligibilityService. Constructor injection does not just improve testability: it improves design by making dependency count visible and the class's responsibilities explicit.

Remember one sentence: constructor injection with final fields gives you immutability, compile safety, fast tests, and a visible signal when a class has too many responsibilities.

---

Share
Use ← → arrow keys to navigate