Every Spring Boot tutorial starts the same way:
@Autowired
private UserService userService;
It works. It's concise. And it's a trap.
After years of debugging production issues and writing tests for legacy codebases, I've stopped using field injection entirely. Here's why.
The Problem With Field Injection
Field injection hides your dependencies. When you look at a class constructor, you should immediately understand what that class needs to function:
// Field injection - dependencies are hidden
@Service
public class OrderService {
@Autowired
private UserService userService;
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
@Autowired
private NotificationService notificationService;
@Autowired
private AuditService auditService;
}
Five dependencies. You only discover this by scanning the entire class. Now imagine this class has 500 lines of code.
Constructor Injection Makes Dependencies Explicit
@Service
public class OrderService {
private final UserService userService;
private final PaymentService paymentService;
private final InventoryService inventoryService;
public OrderService(
UserService userService,
PaymentService paymentService,
InventoryService inventoryService
) {
this.userService = userService;
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
}
Now the constructor screams: "This class does too much." That's valuable feedback.
Testing Becomes Trivial
With field injection, you need reflection or Spring's test context to inject mocks:
// Painful
@SpringBootTest
class OrderServiceTest {
@MockBean
private UserService userService;
// ... slow, heavy tests
}
With constructor injection:
// Simple
class OrderServiceTest {
private OrderService orderService;
@BeforeEach
void setUp() {
orderService = new OrderService(
mock(UserService.class),
mock(PaymentService.class),
mock(InventoryService.class)
);
}
}
No Spring context. No reflection. Fast, isolated unit tests.
The Final Keyword Matters
Notice the final keyword on each field. This guarantees:
- Dependencies are set once at construction
- No accidental reassignment
- Thread safety without synchronization
- Compiler enforces completeness
When I Still Use @Autowired
Setter injection for optional dependencies:
@Autowired(required = false)
public void setMetricsService(MetricsService metricsService) {
this.metricsService = metricsService;
}
That's it. Everything else gets constructor injection.
The Rule
If your constructor has more than 4-5 parameters, your class is doing too much. Constructor injection makes this obvious. Field injection hides it.
Explicit dependencies. Testable code. Simple rule.