Constructor Injection with Final Fields in Spring: Benefits and Practical Refactoring
Learn why using constructor injection with final fields in Spring improves immutability, testability, and early detection of circular dependencies, plus see a refactoring example.
14 Oct 2025, 14:04 UTC

Many Spring developers start with @Autowired on private fields because it looks clean and concise. However, this convenience creates a hidden coupling to the Spring container, makes unit testing cumbersome, and masks architectural flaws like circular dependencies until runtime. Moving to constructor injection isn't just a stylistic choice; it is an engineering decision to ensure your components are immutable and self-validating.
Enforcing Immutability and Thread Safety
When you use field injection, you cannot declare your dependencies as final. This means the state of your bean could theoretically be changed after the application context has finished initialization. Since Spring beans are singletons by default, having mutable dependencies across multiple threads is a significant safety risk.
By using constructor injection, you can define your collaborators as final fields. This guarantees that once the bean is instantiated, its dependencies are locked in place. This makes the bean inherently more robust and easier to reason about in a concurrent environment.
Testing Without the Spring Overhead
One of the biggest pain points with field injection is unit testing. Because the dependencies are hidden in private fields, you cannot simply pass a mock into the class. You are forced to either use reflection‑based utilities or start a heavy @SpringBootTest context which slows down your test suite significantly.
With constructor injection, you can instantiate your class in a plain JUnit test. You simply pass your mocks or stubs into the constructor. This keeps your unit tests fast, isolated, and independent of the framework.
Early Failure on Circular Dependencies
Field injection can hide circular dependencies (where Bean A needs Bean B, and Bean B needs Bean A). The application might start up, but fail much later when a specific code path is hit and a dependency is not fully initialized.
Constructor injection forces Spring to resolve all dependencies at bean creation. If two beans require each other via constructors, Spring will throw a BeanCurrentlyInCreationException immediately upon startup. This \"fail fast\" behavior is a gift; it highlights a design issue that needs to be refactored before the code ever reaches production.
Practical Example: Refactoring to Constructor Injection
Consider a standard service that requires a repository. Note that since Spring Framework 4.3, if a class has only one constructor, the @Autowired annotation is optional.
// The Old Way: Field Injection
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUser(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
// The Modern Way: Constructor Injection
@Service
public class UserService {
private final UserRepository userRepository;
// No @Autowired needed for single constructor
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUser(Long id) {
return userRepository.findById(id).orElseThrow();
}
}Here is how you would test the modern version without any Spring‑specific annotations:
@Test
void testGetUser() {
// Arrange
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo); // Plain Java!
when(mockRepo.findById(1L)).thenReturn(Optional.of(new User()));
// Act
User result = service.getUser(1L);
// Assert
assertNotNull(result);
}Trade‑offs and Limitations
The primary drawback of constructor injection is \"constructor bloat\". If you find your constructor has 8 or 10 arguments, it is a clear signal (code smell) that your class is doing too much and should be split into smaller, more focused components.
Additionally, optional dependencies are awkward with constructors. Instead of overloading the constructor, consider using Optional<T> or the @Nullable annotation on the parameter to explicitly signal that the dependency is not required.
To transition, start by replacing your most critical @Autowired fields with final fields and generated constructors.
Actionable Closing
Adopting constructor injection with final fields gives you immutable beans, simpler unit tests, and early detection of circular dependencies. Review your services, replace field injection with constructor‑based constructors, and let the compiler and Spring container help you catch design problems early.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.