When you add @Autowired to a field in IntelliJ IDEA, you’ll see the warning “Field injection is not recommended.” Even though new-hire training taught you field injection, code reviews on the job tell you to “switch to constructor injection.” Let’s sort out why each approach is treated differently.
By the end of this article, you’ll be able to explain the differences between the three approaches along four axes — testability, null safety, immutability, and circular dependency detection — and write new code with confidence about which style to use. If the concept of DI itself is still fuzzy, start with What is DI.
What are the three approaches?
Spring Boot has three ways to receive dependency Beans: constructor injection receives them through constructor arguments, setter injection uses setter methods, and field injection attaches @Autowired directly to fields.
Since Spring 4.3, when a class has only one constructor, DI works even if you omit @Autowired. That’s why you don’t see @Autowired much in modern Spring Boot code.
Writing the same service in three patterns
Let’s use a UserService that depends on UserRepository. First, field injection.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User find(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
Next, setter injection.
@Service
public class UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User find(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
And constructor injection. We’ve omitted @Autowired.
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User find(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
At a glance, field injection looks shortest and most appealing. But what matters is “what’s guaranteed.”
The difference in testability
This is the biggest reason constructor injection is recommended. In unit tests, you can just new it up and pass in mocks.
@Test
void findUser() {
UserRepository repo = mock(UserRepository.class);
when(repo.findById(1L)).thenReturn(Optional.of(new User(1L, "taro")));
UserService service = new UserService(repo);
assertThat(service.find(1L).getName()).isEqualTo("taro");
}
With field injection, you need to inject a mock directly into a private field, which means using reflection or starting up @SpringBootTest. Tests become heavy, and you lose the ability to write POJO tests that don’t depend on Spring.
Setter injection works if you call setUserRepository(...), but as dependencies grow, it’s quite common to forget a call and trip over an NPE.
Null safety and immutability
With constructor injection, you can use final. Since final fields must be initialized at the time of the constructor call, a constructed UserService instance can never have a null dependency.
With field or setter injection, the language syntactically allows methods to be called before Spring sets the values. For readers of the code, “when are dependencies ready?” stays opaque.
The behavior when a required dependency is missing also differs. With constructor injection, the application fails at startup, so you catch it before deployment. With field injection, you may not notice until runtime.
When are circular dependencies caught?
Consider a circular dependency where AService needs BService and BService needs AService. With constructor injection, the application fails at startup with BeanCurrentlyInCreationException.
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| aService defined in file [.../AService.class]
↑ ↓
| bService defined in file [.../BService.class]
└─────┘
With field/setter injection, Spring can create one side first and inject the other afterward, so the application may start up even with a cycle. Since it surfaces as an NPE or infinite loop at runtime, tracking down the cause is harder.
The earlier you notice a design problem, the better. Since Spring Boot 2.6, circular dependencies are forbidden by default. This too is a message: “If you have a cycle, reconsider your design.”
Cutting boilerplate with Lombok’s @RequiredArgsConstructor
The only downside of constructor injection was boilerplate, and Lombok’s @RequiredArgsConstructor eliminates it.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final NotificationService notificationService;
public User find(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
A constructor that takes only final fields as arguments is generated automatically. When adding dependencies, you just add one line of private final ..., so the changes are minimal. For Lombok setup, see How to use Lombok.
The reasoning behind IntelliJ’s warning
“Field injection is not recommended” has three reasons. First, it’s hard to test. Second, you can’t use final, so you can’t preserve immutability. Third, dependencies don’t appear in the constructor signature and become hidden.
When refactoring existing code, the change is mechanical. Remove @Autowired from the field, add final, and put @RequiredArgsConstructor on the class — that alone clears the warning.
When setter injection shines
It’s not all constructors all the time; setters have their place. One case is optional dependencies.
@Autowired(required = false)
public void setMetricsClient(MetricsClient client) {
this.metricsClient = client;
}
Another is as a last resort when a circular dependency is unavoidable by design. That said, as discussed below, you should really fix the design.
Solve circular dependencies through design
With @Lazy, circular dependencies will compile on the surface.
public AService(@Lazy BService bService) {
this.bService = bService;
}
But it’s a band-aid. It’s a sign that responsibilities should be separated. A common pattern is to refactor by extracting shared logic that both sides need into a third Bean. Alternatively, replacing bidirectional calls with events via ApplicationEventPublisher makes the compile-time dependency one-directional.
For splitting component responsibilities, reading What is @Component and Bean scope explained together helps organize your thinking.
Summary
For new code, the rule is constructor injection + @RequiredArgsConstructor. Tests are easy to write, you get immutable objects that can’t be null, and circular dependencies are caught at startup.
Reserve setters for optional dependencies, and limit field injection to test classes or throwaway script-like code — that’s the practical landing spot. Think of IntelliJ’s warning as a reminder to tidy up your design, and when you see it, calmly fix it.