Definition
- 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
finalfields; the bean cannot be created without all dependencies. - Field injection --
@Autowiredplaced on a private field; convenient but hides dependencies, preventsfinal, and makes testing hard. - Setter injection --
@Autowiredon a setter method; used for truly optional dependencies. - @RequiredArgsConstructor -- Lombok annotation that generates a constructor for all
finalfields, 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.
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;
}
}
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.
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
finalbecause they are set after construction. - No compile-time safety. A missing dependency compiles fine and fails at runtime.
- Hard to test. You need
@SpringBootTestor 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.
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 |
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);
}
}
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.
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:
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
}
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:
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;
}
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)
);
}
}
Field injection tests: 3200ms (loading Spring context)
Constructor injection tests: 48ms (plain Mockito)
WRONG -- too many constructor parameters (SRP violation):
@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
@Autowiredfield. - 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
@Autowiredon 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)orObjectProvider.) - Is
@RequiredArgsConstructorfrom Spring? (No, it is from Lombok. Spring just detects the generated constructor.)
Visual Overview
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.
---