Spring Boot Constructor Injection: Why It Matters and How to Use It
Dependency injection is one of the fundamental concepts behind Spring and Spring Boot applications. It allows a class to receive the objects it depends on rather than creating those objects itself.
Spring Boot Constructor Injection: Why It Matters and How to Use It
Introduction
Dependency injection is one of the fundamental concepts behind Spring and Spring Boot applications. It allows a class to receive the objects it depends on rather than creating those objects itself.
Spring supports several ways to inject dependencies, including field injection, setter injection, and constructor injection.
Among these approaches, constructor injection is generally the preferred approach for required dependencies.
In this article, we'll look at how constructor injection works in Spring Boot, why it is useful, and how it compares with field injection.
What Is Dependency Injection?
Consider a service that needs a repository to retrieve data.
Without dependency injection, you might write:
public class UserService {
private UserRepository userRepository;
public UserService() {
this.userRepository = new UserRepository();
}
}
The problem is that UserService is now responsible for creating its own dependency.
This creates tight coupling between the two classes.
With dependency injection, the responsibility for creating and providing the dependency is moved to the Spring container.
Conceptually:
Spring Container
|
| creates
v
UserRepository
|
| injects
v
UserService
The service simply declares what it needs.
What Is Constructor Injection?
Constructor injection means that dependencies are provided through a class constructor.
For example:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
Spring creates the UserRepository bean and passes it into the UserService constructor.
The important part is:
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
The dependency is available immediately when the object is created.
A Complete Example
Let's build a simple Spring Boot example.
First, create a repository:
@Repository
public class UserRepository {
public String findUser() {
return "Julius Fasema";
}
}
Then create a service that depends on it:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String getUser() {
return userRepository.findUser();
}
}
Finally, inject the service into a controller:
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public String getUser() {
return userService.getUser();
}
}
Spring manages the entire dependency chain:
UserController
|
v
UserService
|
v
UserRepository
You don't need to manually create any of these objects with new.
How Spring Knows Which Dependency to Inject
Spring detects classes annotated with components such as:
@Component
@Service
@Repository
@Controller
@RestController
These classes become Spring-managed beans.
When Spring creates UserService, it sees that its constructor requires:
UserRepository
Because UserRepository is also a Spring bean, Spring provides it automatically.
In modern Spring applications, if a class has only one constructor, you generally don't need to add @Autowired.
For example:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
This is enough.
You can also explicitly use @Autowired:
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
But for a single constructor, the annotation is unnecessary.
Why Use final?
Notice that the dependency was declared as:
private final UserRepository userRepository;
Using final communicates an important design decision:
This dependency is required by the class and should not change after construction.
The field must therefore be initialized in the constructor.
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
This makes the dependency explicit and prevents it from accidentally being reassigned later.
Constructor Injection vs Field Injection
You may have seen code like this:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
This is called field injection.
Although Spring supports it, constructor injection has several advantages.
1. Dependencies are explicit
With constructor injection:
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
Anyone looking at the class immediately knows what the service requires.
With field injection, the dependencies are hidden inside the class fields.
2. Dependencies can be immutable
Constructor injection allows:
private final UserRepository userRepository;
Field injection generally requires the field to remain non-final.
Constructor injection therefore works naturally with immutable dependencies.
3. Easier unit testing
Consider a service using constructor injection:
UserRepository repository = mock(UserRepository.class);
UserService service = new UserService(repository);
The dependency can be supplied directly.
You don't need Spring to start the application just to create the service.
This makes unit tests faster and simpler.
4. Required dependencies cannot be forgotten
Suppose UserService requires two dependencies:
private final UserRepository userRepository;
private final EmailService emailService;
The constructor makes this requirement explicit:
public UserService(
UserRepository userRepository,
EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
Anyone creating the service must provide both dependencies.
This helps prevent partially initialized objects.
Constructor Injection with Multiple Dependencies
Real-world services often depend on multiple components.
For example:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final EmailService emailService;
public OrderService(
OrderRepository orderRepository,
PaymentService paymentService,
EmailService emailService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
this.emailService = emailService;
}
}
Spring will resolve all three dependencies when creating OrderService.
The service now clearly communicates its dependencies.
Constructor Injection and Interfaces
Constructor injection becomes particularly useful when your application depends on interfaces.
Suppose you have:
public interface PaymentService {
void processPayment();
}
And an implementation:
@Service
public class FlutterwavePaymentService implements PaymentService {
@Override
public void processPayment() {
// Payment implementation
}
}
You can inject the interface:
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
This reduces the service's dependency on a specific implementation.
It also makes replacing the implementation easier.
What If There Are Multiple Implementations?
Suppose you have:
@Service
public class FlutterwavePaymentService
implements PaymentService {
}
and:
@Service
public class PaystackPaymentService
implements PaymentService {
}
Now Spring has two possible implementations of PaymentService.
You can use @Qualifier:
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(
@Qualifier("flutterwavePaymentService")
PaymentService paymentService) {
this.paymentService = paymentService;
}
}
This tells Spring exactly which implementation should be injected.
Another option is to use @Primary on the implementation you want Spring to prefer.
Constructor Injection and Lombok
Developers sometimes use Lombok to reduce constructor boilerplate.
For example:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
}
Lombok generates a constructor for the final fields.
The resulting code is conceptually similar to:
public UserService(
UserRepository userRepository,
EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
This can make classes shorter, but it's important for developers to understand the underlying constructor injection mechanism rather than relying on the annotation without knowing what it generates.
Constructor Injection Makes Dependencies Visible
One of the biggest benefits of constructor injection is that the class's dependencies are visible from its API.
Consider:
@Service
public class InvoiceService {
private final InvoiceRepository invoiceRepository;
private final PaymentService paymentService;
private final EmailService emailService;
public InvoiceService(
InvoiceRepository invoiceRepository,
PaymentService paymentService,
EmailService emailService) {
this.invoiceRepository = invoiceRepository;
this.paymentService = paymentService;
this.emailService = emailService;
}
}
A developer can immediately understand what InvoiceService needs.
This is useful when working on large applications where classes may have many dependencies.
Too Many Constructor Dependencies Can Be a Warning Sign
Constructor injection also exposes a useful architectural problem.
Consider:
public OrderService(
OrderRepository orderRepository,
PaymentService paymentService,
EmailService emailService,
InventoryService inventoryService,
NotificationService notificationService,
ShippingService shippingService,
DiscountService discountService,
AuditService auditService) {
}
If a class requires eight or ten dependencies, the problem may not be constructor injection.
The class itself may be doing too much.
This can be a signal that the class should be divided into smaller components with clearer responsibilities.
In this way, constructor injection can help reveal design problems rather than hide them.
Constructor Injection and Circular Dependencies
Another issue worth understanding is circular dependency.
Imagine:
ServiceA → ServiceB
↑ |
└─────────┘
Where ServiceA requires ServiceB, while ServiceB requires ServiceA.
With constructor injection:
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
and:
@Service
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
Spring can detect the circular dependency and fail during application startup.
Rather than treating this as merely a Spring problem, consider it an architectural warning.
The better solution is often to redesign the relationship between the services and remove the circular dependency.
A Practical Spring Boot Pattern
A typical Spring Boot application can use constructor injection consistently across its layers:
Controller
↓
Service
↓
Repository
For example:
@RestController
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
}
Then:
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
}
And finally:
@Repository
public interface ProductRepository
extends JpaRepository<Product, Long> {
}
Each layer receives its dependency through its constructor.
This produces a clear dependency chain and keeps responsibilities separated.
Best Practices
When working with dependency injection in Spring Boot:
Prefer constructor injection for required dependencies.
Declare injected dependencies as
private finalwhere appropriate.Avoid field injection for application code.
Don't use
@Autowiredwhen a class has only one constructor.Use interfaces when abstraction provides a meaningful architectural benefit.
Use
@Qualifierwhen multiple beans implement the same interface.Treat classes with excessive constructor dependencies as potential design problems.
Avoid circular dependencies by improving the application design.
Keep controllers thin and place business logic in services.
Write unit tests that instantiate classes with their dependencies directly where practical.
Final Thoughts
Constructor injection is more than just another way of injecting dependencies in Spring Boot. It encourages explicit dependencies, supports immutable fields, improves testability, and can expose architectural problems early.
Instead of allowing a class to quietly acquire the objects it needs, constructor injection makes those requirements part of the class's construction.
For small Spring Boot applications, the difference may seem minor. As an application grows, however, clear dependency management becomes increasingly important.
A simple pattern such as:
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
can therefore contribute to cleaner, more maintainable Spring Boot applications.
Tags: Spring Boot, Java, Dependency Injection, Constructor Injection, Spring Framework