Dependency Injection in Spring Boot
Revisit dependency injection in the context of Spring Boot - constructor injection, wiring a service into a controller, and why field injection is discouraged.
Introduction
Dependency injection (DI) is the mechanism that makes Spring applications loosely coupled and easy to test, and it is just as central in Spring Boot as it is in plain Spring. The difference in Boot is mostly about ceremony: auto-configuration and component scanning mean you rarely write XML or manual bean wiring, so DI tends to feel almost invisible. In this lesson you will revisit dependency injection specifically in the Spring Boot context - wiring a service into a controller with constructor injection, the recommended approach for virtually every project.
- A quick recap of what dependency injection solves.
- How constructor injection works in a Spring Boot application.
- How to inject a service into a REST controller.
- Why field injection with @Autowired is discouraged.
- How to inject one of several implementations of an interface.
A Quick Recap of DI
Without DI, a class that needs a collaborator typically creates it directly with new, which hard-codes the dependency and makes the class difficult to test in isolation. With DI, the class simply declares what it needs - usually through its constructor - and the Spring container supplies a ready-made instance at startup. The class never calls new on its dependencies; it just receives them.
// Without DI - tightly coupled, hard to testpublic class OrderService { private final EmailSender emailSender = new SmtpEmailSender();}
// With DI - the container supplies the dependency@Servicepublic class OrderService { private final EmailSender emailSender;
public OrderService(EmailSender emailSender) { this.emailSender = emailSender; }}As long as EmailSender has exactly one implementation annotated with a stereotype like @Component or @Service, Spring Boot finds it during component scanning and passes it into the OrderService constructor automatically - no configuration file required.
Constructor Injection in Boot
Constructor injection means dependencies are passed in through the class constructor and stored in final fields. Since Spring 4.3, if a class has exactly one constructor, the @Autowired annotation on it is optional - Spring Boot will use that constructor automatically to build the bean.
@Servicepublic class TaskService {
private final TaskRepository taskRepository;
// @Autowired is optional here because there is only one constructor public TaskService(TaskRepository taskRepository) { this.taskRepository = taskRepository; }
public List<Task> findAll() { return taskRepository.findAll(); }}Marking the field final gives you a compiler-enforced guarantee that the dependency is set exactly once, at construction time, and can never be reassigned later. This also makes the class trivially testable - in a unit test you can call new TaskService(mockRepository) directly, with no Spring container involved at all.
Injecting a Service into a Controller
The most common wiring pattern in a Spring Boot web application is a controller depending on a service, and the service depending on a repository. Each layer only knows about the layer directly beneath it.
@RestController@RequestMapping("/api/tasks")public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) { this.taskService = taskService; }
@GetMapping public List<Task> getAllTasks() { return taskService.findAll(); }
@PostMapping public Task createTask(@RequestBody Task task) { return taskService.save(task); }}Click Run to see what this code prints.
Click Run to see what this code prints.
Notice that TaskController never constructs a TaskService itself and never touches TaskRepository directly. Each class depends only on its immediate collaborator, which keeps responsibilities cleanly separated and makes each layer easy to swap or mock independently.
Why Not Field Injection?
Field injection - annotating a field directly with @Autowired instead of using a constructor - is common in older tutorials but is now widely discouraged, including by the Spring team itself.
// Discouraged: field injection@Servicepublic class TaskService {
@Autowired private TaskRepository taskRepository;}| Constructor Injection | Field Injection |
|---|---|
| Dependencies can be marked final | Fields cannot be final, so they are mutable after construction |
| Missing dependency fails fast at startup | Missing dependency can produce a confusing NullPointerException later |
| Easy to unit test with plain `new` | Requires a Spring context or reflection tricks to test |
| Makes "too many dependencies" visually obvious | Hides an overloaded constructor behind a long list of fields |
A constructor with eight parameters is an obvious signal that a class is doing too much. A class with eight @Autowired fields hides that same problem in plain sight, which is one more reason constructor injection is the recommended default.
Injecting Multiple Implementations
When more than one bean implements the same interface, Spring cannot guess which one you want and throws a NoUniqueBeanDefinitionException at startup. Use @Qualifier to tell it exactly which implementation to inject.
public interface NotificationSender { void send(String message);}
@Component("emailSender")public class EmailNotificationSender implements NotificationSender { public void send(String message) { System.out.println("Email: " + message); }}
@Component("smsSender")public class SmsNotificationSender implements NotificationSender { public void send(String message) { System.out.println("SMS: " + message); }}
@Servicepublic class AlertService {
private final NotificationSender sender;
public AlertService(@Qualifier("emailSender") NotificationSender sender) { this.sender = sender; }}If one implementation should be the default in almost every case, annotate it with @Primary instead of adding @Qualifier everywhere it is injected. Reserve @Qualifier for the specific spots that need the non-default implementation.
Common Mistakes
- Using field injection out of habit instead of constructor injection.
- Forgetting to annotate an implementation class with @Component, @Service, or @Repository, then wondering why Spring cannot find a bean to inject.
- Having two implementations of an interface with no @Qualifier or @Primary, causing a startup failure.
- Injecting a repository directly into a controller, skipping the service layer that should hold business logic.
- Creating circular dependencies, such as service A depending on service B which depends back on service A.
Best Practices
- Default to constructor injection for all required dependencies.
- Mark injected fields as private final.
- Keep controllers thin - they should delegate to services, not contain business logic.
- Use @Qualifier or @Primary explicitly whenever multiple implementations exist.
- If a constructor is growing very long, treat it as a signal to split the class rather than switching to field injection.
Frequently Asked Questions
No, not if the class has only one constructor. Spring Boot uses it automatically. @Autowired is only required if a class has multiple constructors and you need to tell Spring which one to use for injection.
Setter injection exists and is occasionally useful for optional dependencies, but constructor injection is preferred for required dependencies because it guarantees the object is never in a partially-initialized state.
Yes, using @Value("${some.property}") on a constructor parameter, though for groups of related properties a @ConfigurationProperties class is usually cleaner, as covered in an earlier lesson.
Key Takeaways
- Dependency injection lets classes declare what they need instead of constructing it themselves.
- Constructor injection with final fields is the recommended approach in Spring Boot.
- Field injection works but hides missing-dependency errors and design smells.
- Use @Qualifier or @Primary to disambiguate when multiple beans implement the same interface.
- Keep the controller -> service -> repository chain intact rather than skipping layers.
Summary
Dependency injection in Spring Boot works the same way it does in plain Spring, but auto-configuration and component scanning remove most of the manual wiring. Favor constructor injection with final fields, keep each layer depending only on the layer beneath it, and reach for @Qualifier or @Primary when a bean has more than one implementation.