1. Introduction
Constructor Injection is a dependency injection technique where a class receives the objects it depends on through its constructor.
In Java and Spring Boot projects, service classes commonly depend on repositories, API clients, validators, mappers, configuration components, or other services.
Instead of creating those dependencies inside the class or injecting them into mutable fields, Constructor Injection makes the required dependencies explicit when the object is created.
Example:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) {
this.orderRepository = orderRepository;
this.paymentClient = paymentClient;
}
}This approach is especially useful during code reviews because it clearly shows what a class needs in order to work correctly.
Constructor Injection improves:
- Dependency visibility
- Testability
- Immutability of dependency references
- Class design
- Failure detection during application startup
- Maintainability
In modern Spring Boot applications, Constructor Injection is generally the preferred approach for required dependencies.
2. What This Topic Means
A Java class usually cannot perform all work by itself.
For example, an OrderService may need:
OrderRepositoryto access the databasePaymentClientto call a payment serviceInventoryServiceto reserve productsNotificationServiceto send confirmation messages
These objects are dependencies of OrderService.
With Constructor Injection, the class receives these dependencies when the class instance is created.
Example:
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) {
this.orderRepository = orderRepository;
this.paymentClient = paymentClient;
}
}The class does not decide how those dependencies are created.
It only declares:
"I cannot work correctly unless you provide these dependencies."
In a Spring Boot application, the Spring container creates the dependency objects and passes them to the constructor.
For a Spring-managed component with a single constructor, @Autowired normally does not need to be added explicitly.
Example:
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}Spring detects the constructor automatically and injects the required bean.
3. Why It Matters in Real Projects
Constructor Injection is not merely a syntax preference.
It directly affects how understandable, testable, and maintainable a class becomes.
Readability
The constructor shows every required dependency.
When a developer opens a class such as:
public OrderService(
OrderRepository orderRepository,
PaymentClient paymentClient,
InventoryService inventoryService) {
this.orderRepository = orderRepository;
this.paymentClient = paymentClient;
this.inventoryService = inventoryService;
}the developer immediately understands which collaborators the service requires.
With field injection, dependencies may be scattered throughout the class.
Maintainability
Constructor Injection encourages dependency references to be declared final.
Example:
private final OrderRepository orderRepository;This prevents another method from accidentally replacing the dependency.
Testability
Dependencies can easily be supplied using mocks or stubs without starting Spring.
Example:
OrderRepository repository = mock(OrderRepository.class);
PaymentClient paymentClient = mock(PaymentClient.class);
OrderService service = new OrderService(repository, paymentClient);This makes fast unit testing straightforward.
Debugging
Missing required dependencies usually become visible when the object is created rather than much later when a method happens to access a null field.
Reliability
A successfully created object is more likely to be in a valid state because required dependencies must already exist.
Team Development
Constructor Injection gives reviewers a clear view of the class dependency graph.
A reviewer can quickly notice that a service has accumulated too many responsibilities if the constructor requires ten or fifteen dependencies.
4. Core Concept
The core idea behind Constructor Injection is:
Required dependencies should be supplied when an object is created.
Consider a payment-processing service.
@Service
public class PaymentService {
private final PaymentRepository paymentRepository;
private final PaymentGatewayClient paymentGatewayClient;
public PaymentService(
PaymentRepository paymentRepository,
PaymentGatewayClient paymentGatewayClient) {
this.paymentRepository = paymentRepository;
this.paymentGatewayClient = paymentGatewayClient;
}
}The PaymentService cannot exist normally without:
PaymentRepositoryPaymentGatewayClient
Therefore they are constructor parameters.
Dependency Inversion
Constructor Injection works especially well when the class depends on abstractions.
Example:
public interface PaymentGateway {
PaymentResult charge(PaymentRequest request);
}Implementation:
@Component
public class RazorpayPaymentGateway implements PaymentGateway {
@Override
public PaymentResult charge(PaymentRequest request) {
return new PaymentResult(true);
}
}Service:
@Service
public class PaymentService {
private final PaymentGateway paymentGateway;
public PaymentService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}The business service depends on the PaymentGateway abstraction instead of manually constructing a particular implementation.
This improves flexibility and testability.
Object Validity
Constructor Injection helps enforce valid object creation.
Bad:
PaymentService service = new PaymentService();
service.setPaymentGateway(paymentGateway);Between those two statements, the object exists without a required dependency.
Constructor-based design avoids that intermediate invalid state.
5. Important Rules
When reviewing Constructor Injection in Java and Spring Boot applications, follow these practical rules:
- Use Constructor Injection for required dependencies.
- Declare injected dependencies
finalwhere possible. - Do not create infrastructure dependencies manually using
newinside business services. - Avoid field injection for ordinary application dependencies.
- Do not add
@Autowiredunnecessarily when a Spring component has only one constructor. - Prefer abstractions when multiple implementations or testing flexibility are required.
- Avoid using constructors with excessive numbers of dependencies.
- Treat an oversized constructor as a possible Single Responsibility Principle warning.
- Do not pass unrelated configuration values individually when a configuration object would be clearer.
- Do not make a dependency nullable unless it is genuinely optional.
- Avoid hiding dependencies through static utility access or service locators.
- Keep constructors focused on assigning dependencies rather than executing business logic.
- Do not perform database calls, API calls, or heavy computation inside constructors.
6. Bad Code Example
Consider the following order-processing service.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentClient paymentClient;
@Autowired
private NotificationService notificationService;
public OrderResponse placeOrder(OrderRequest request) {
Order order = new Order();
order.setCustomerId(request.getCustomerId());
order.setAmount(request.getAmount());
Order savedOrder = orderRepository.save(order);
PaymentResult paymentResult = paymentClient.processPayment(
savedOrder.getId(),
savedOrder.getAmount());
if (paymentResult.isSuccessful()) {
notificationService.sendOrderConfirmation(savedOrder.getId());
}
return new OrderResponse(savedOrder.getId(), paymentResult.isSuccessful());
}
}This code may work correctly when Spring creates the object.
However, the dependency design is weaker than necessary.
7. Problems in the Bad Code
Hidden Dependencies
The dependencies are injected directly into private fields.
A developer looking only at object construction cannot immediately see that OrderService requires three collaborators.
Mutable Dependencies
The fields cannot conveniently be declared final.
Therefore the references are mutable for the lifetime of the object.
Difficult Plain Unit Testing
Creating the service directly is inconvenient.
This does not work correctly:
OrderService service = new OrderService();The dependencies remain uninitialized unless reflection, Spring, or another injection mechanism populates them.
Invalid Object State
The class can theoretically exist without its required dependencies.
If the object is created outside Spring, calling:
service.placeOrder(request);may cause a NullPointerException.
Framework Coupling
The class relies directly on Spring's field-injection behavior.
The business service becomes harder to instantiate as an ordinary Java class.
Reduced Design Visibility
During a Pull Request review, constructor dependencies provide useful architectural information.
Field injection hides some of that information.
8. Code Review Findings
A senior Java developer reviewing the previous code should notice:
OrderRepository,PaymentClient, andNotificationServiceare required dependencies.- All three are injected using fields instead of through the constructor.
- The dependency references are mutable.
- Plain Java unit testing becomes unnecessarily difficult.
- The class can be instantiated in an invalid state outside the Spring container.
- Spring annotations are controlling object validity instead of the Java type design.
- These dependencies should probably be constructor parameters.
- The dependencies should be declared
final. - Explicit
@Autowiredfields are unnecessary for this design.
The reviewer should not request Constructor Injection only because of a coding-style rule.
The important architectural reason is that the class should clearly declare everything it requires to function.
9. Reviewer Comment Example
A practical PR review comment could be:
Could we inject these required dependencies through the constructor and make the fields
final? This makes the dependencies explicit, prevents reassignment, and allows the service to be unit-tested without relying on Spring field injection.
Another appropriate comment:
OrderRepository,PaymentClient, andNotificationServiceappear to be mandatory collaborators. Please consider Constructor Injection so thatOrderServicecannot be created without them.
10. Improved Code
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
private final NotificationService notificationService;
public OrderService(
OrderRepository orderRepository,
PaymentClient paymentClient,
NotificationService notificationService) {
this.orderRepository = orderRepository;
this.paymentClient = paymentClient;
this.notificationService = notificationService;
}
public OrderResponse placeOrder(OrderRequest request) {
Order order = new Order();
order.setCustomerId(request.getCustomerId());
order.setAmount(request.getAmount());
Order savedOrder = orderRepository.save(order);
PaymentResult paymentResult = paymentClient.processPayment(
savedOrder.getId(),
savedOrder.getAmount());
if (paymentResult.isSuccessful()) {
notificationService.sendOrderConfirmation(savedOrder.getId());
}
return new OrderResponse(
savedOrder.getId(),
paymentResult.isSuccessful());
}
}Because the class contains only one constructor, modern Spring versions can use it for dependency injection without explicit @Autowired.
11. Improved Code Explanation
Dependencies Are Explicit
The constructor clearly shows that OrderService requires:
OrderRepositoryPaymentClientNotificationService
There are no hidden framework-injected dependencies.
Fields Are Final
private final OrderRepository orderRepository;The dependency reference cannot accidentally be reassigned after object construction.
Easier Unit Testing
The service can be instantiated directly.
OrderService service = new OrderService(
orderRepository,
paymentClient,
notificationService);No Spring container is required for an ordinary unit test.
Invalid Construction Is Harder
This is no longer possible:
new OrderService();The compiler requires all constructor arguments.
Reduced Framework Dependency
Spring still manages the service in production, but the Java class itself does not depend on field injection to become usable.
Better Code Review Visibility
The constructor acts as a concise list of collaborators.
If the list becomes very large, reviewers immediately notice that the class may be doing too much.
12. Bad Code vs Improved Code
| Area | Field Injection | Constructor Injection |
|---|---|---|
| Dependency visibility | Dependencies are hidden inside fields | Dependencies are visible in constructor |
| Immutability | Fields usually remain mutable | Fields can be final |
| Unit testing | Often requires framework support or reflection | Direct object creation is simple |
| Object validity | Object can exist before fields are injected | Required dependencies are supplied at construction |
| Framework coupling | Stronger dependency on injection mechanism | Class remains easier to use as plain Java |
| Code review | Dependency count is less obvious | Dependency graph is immediately visible |
| Refactoring | Hidden dependencies can be missed | Constructor changes expose dependency changes |
Constructor Injection usually does not materially change runtime performance.
Its main advantages concern design quality, maintainability, correctness, and testability.
13. Real Project Scenario
Consider an e-commerce checkout microservice.
A CheckoutService depends on:
CartRepositoryInventoryClientPaymentGatewayOrderRepositoryNotificationPublisher
Initially, developers use field injection.
@Autowired
private PaymentGateway paymentGateway;Later, another team creates CheckoutService directly in a unit test.
The object is successfully instantiated, but paymentGateway remains null.
The test fails with a NullPointerException several calls later.
The immediate error appears to be inside payment processing, but the actual design problem occurred when the object was created without its required dependencies.
With Constructor Injection:
CheckoutService checkoutService = new CheckoutService(
cartRepository,
inventoryClient,
paymentGateway,
orderRepository,
notificationPublisher);The compiler forces the developer to provide every required collaborator.
This makes object construction safer and tests clearer.
14. Production Impact
Using field injection does not automatically create production failures.
However, weak dependency design can contribute to several problems.
Difficult Maintenance
Hidden dependencies make services harder to understand and refactor.
Runtime Null Failures Outside Spring
If a class is instantiated manually somewhere in production code, injected fields may remain null.
Poor Test Coverage
When classes are difficult to instantiate independently, teams may rely excessively on expensive Spring integration tests rather than focused unit tests.
Architectural Complexity
Because field injection makes adding another dependency very easy, oversized services can grow without the warning signal of an increasingly large constructor.
Difficult Debugging
A failure caused by incomplete object initialization may appear far from the location where the object was created.
Constructor Injection moves these problems closer to object creation.
15. Common Developer Mistakes
Using Field Injection Everywhere
Example:
@Autowired
private CustomerRepository customerRepository;This is convenient but hides required dependencies.
Adding @Autowired to a Single Constructor Without Need
Example:
@Autowired
public CustomerService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}This is generally unnecessary for a Spring component with one constructor.
Creating Dependencies Manually
Bad:
private final PaymentClient paymentClient = new PaymentClient();This bypasses dependency management and makes testing harder.
Injecting Concrete Classes Unnecessarily
Example:
private final StripePaymentGateway stripePaymentGateway;If the business service logically depends on the PaymentGateway abstraction, depending directly on one implementation reduces flexibility.
Constructor with Too Many Dependencies
Example:
public OrderService(
OrderRepository repository,
PaymentClient paymentClient,
InventoryClient inventoryClient,
CustomerService customerService,
DiscountService discountService,
TaxService taxService,
NotificationService notificationService,
AuditService auditService,
MetricsService metricsService,
FraudService fraudService) {
}The problem is not Constructor Injection.
The constructor has exposed a deeper design problem: OrderService may have too many responsibilities.
Performing Work Inside the Constructor
Bad:
public ProductService(ProductRepository repository) {
this.repository = repository;
this.products = repository.findAll();
}Constructors should normally establish object state rather than trigger database operations.
Accepting Null Dependencies
Bad:
public PaymentService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}followed later by code that assumes paymentGateway is non-null.
For non-Spring plain Java objects where null can realistically be passed, explicit validation may be appropriate.
Example:
this.paymentGateway = Objects.requireNonNull(
paymentGateway,
"paymentGateway must not be null");16. Edge Cases
Null Dependency
When constructing objects manually, null may be passed.
Example:
new PaymentService(null);For strict domain or library code, fail immediately if null dependencies are not valid.
public PaymentService(PaymentGateway paymentGateway) {
this.paymentGateway = Objects.requireNonNull(
paymentGateway,
"paymentGateway must not be null");
}In a correctly configured Spring application, required bean resolution normally fails during startup rather than injecting null.
Multiple Implementations
Suppose two beans implement:
public interface NotificationSender {
void send(String message);
}with:
@Component
public class EmailNotificationSender implements NotificationSender {
}and:
@Component
public class SmsNotificationSender implements NotificationSender {
}Injecting:
public NotificationService(NotificationSender sender) {
this.sender = sender;
}can become ambiguous.
A qualifier or primary bean strategy may be required.
Example:
public NotificationService(
@Qualifier("emailNotificationSender") NotificationSender sender) {
this.sender = sender;
}Optional Dependency
Do not make a required constructor dependency optional merely to avoid configuration errors.
If functionality is genuinely optional, model that intentionally using an appropriate Spring mechanism or separate component design.
Circular Dependencies
Example:
OrderService -> PaymentService -> OrderServiceConstructor Injection may expose circular dependencies during startup.
That is useful because circular service dependencies often indicate excessive coupling.
The preferred solution is normally redesigning responsibilities rather than switching back to field injection simply to hide the cycle.
Large Dependency Count
A constructor with many dependencies is an architectural signal.
Review whether the class:
- Handles multiple workflows
- Mixes orchestration and business rules
- Performs unrelated integrations
- Violates Single Responsibility Principle
17. Performance Considerations
Constructor Injection normally has no meaningful runtime performance disadvantage compared with field injection in ordinary Spring Boot business applications.
Dependency wiring happens primarily during object creation and application startup.
Runtime Complexity
Accessing an injected constructor field is effectively the same as accessing another object field.
There is no meaningful algorithmic complexity difference.
Startup Cost
The difference between constructor and field assignment is negligible for normal application code.
Real Performance Concern
The more important concern is what developers execute inside constructors.
Avoid:
- Database queries
- Network calls
- File processing
- Large collection loading
- Expensive calculations
Example of problematic constructor behavior:
public CustomerCache(CustomerRepository repository) {
this.customers = repository.findAll();
}This can make bean creation slow and cause startup failures.
Constructor Injection itself is not the performance problem.
Doing expensive work during construction is.
18. Security Considerations
Constructor Injection is primarily a design and maintainability technique rather than a direct security mechanism.
However, it can indirectly improve security architecture.
Explicit Security Dependencies
Consider:
@Service
public class AccountService {
private final AuthorizationService authorizationService;
private final AccountRepository accountRepository;
public AccountService(
AuthorizationService authorizationService,
AccountRepository accountRepository) {
this.authorizationService = authorizationService;
this.accountRepository = accountRepository;
}
}The authorization dependency is visible and clearly required.
Easier Security Testing
A test can inject mocked authorization behavior and verify denied scenarios.
Avoid Injecting Secrets as Plain Strings
Do not scatter sensitive credentials through constructors such as:
public PaymentClient(String apiKey) {
}without appropriate configuration management.
Prefer strongly defined configuration objects managed through secure configuration mechanisms.
Important Limitation
Constructor Injection does not protect against:
- SQL injection
- Authentication bypass
- Authorization bugs
- Sensitive logging
- Data exposure
Those concerns require separate security controls.
19. Testing Considerations
Constructor Injection significantly improves testability.
Consider:
public class PaymentService {
private final PaymentRepository paymentRepository;
private final PaymentGateway paymentGateway;
public PaymentService(
PaymentRepository paymentRepository,
PaymentGateway paymentGateway) {
this.paymentRepository = paymentRepository;
this.paymentGateway = paymentGateway;
}
public PaymentResult process(PaymentRequest request) {
PaymentResult result = paymentGateway.charge(request);
paymentRepository.save(result);
return result;
}
}Positive Unit Test
Verify successful payment processing.
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
@Mock
private PaymentRepository paymentRepository;
@Mock
private PaymentGateway paymentGateway;
private PaymentService paymentService;
@BeforeEach
void setUp() {
paymentService = new PaymentService(
paymentRepository,
paymentGateway);
}
@Test
void shouldSaveSuccessfulPayment() {
PaymentRequest request = new PaymentRequest(1000);
PaymentResult result = new PaymentResult(true);
when(paymentGateway.charge(request)).thenReturn(result);
PaymentResult actual = paymentService.process(request);
assertTrue(actual.isSuccessful());
verify(paymentRepository).save(result);
}
}Negative Test
Simulate gateway failure or exception.
@Test
void shouldPropagateGatewayFailure() {
PaymentRequest request = new PaymentRequest(1000);
when(paymentGateway.charge(request))
.thenThrow(new PaymentGatewayException("Gateway unavailable"));
assertThrows(
PaymentGatewayException.class,
() -> paymentService.process(request));
verifyNoInteractions(paymentRepository);
}Integration Testing
Use Spring integration tests when you need to verify:
- Bean discovery
- Multiple implementations
@Qualifier@Primary- Configuration-based dependencies
- Complete dependency graph creation
Do not start the complete Spring context merely to test ordinary business logic that can be tested using constructor-created objects.
20. Refactoring Guidelines
When converting existing field-injected code to Constructor Injection, avoid changing unrelated business behavior in the same refactoring.
Suppose the existing class contains:
@Service
public class InventoryService {
@Autowired
private InventoryRepository inventoryRepository;
@Autowired
private WarehouseClient warehouseClient;
public InventoryResult reserve(InventoryRequest request) {
return warehouseClient.reserve(request);
}
}Step 1: Identify Required Dependencies
Both collaborators appear required.
Step 2: Make Fields Final
private final InventoryRepository inventoryRepository;
private final WarehouseClient warehouseClient;Step 3: Add Constructor
public InventoryService(
InventoryRepository inventoryRepository,
WarehouseClient warehouseClient) {
this.inventoryRepository = inventoryRepository;
this.warehouseClient = warehouseClient;
}Step 4: Remove Field Injection
Remove @Autowired from fields.
Step 5: Run Unit Tests
Verify existing behavior has not changed.
Step 6: Run Application Context Tests
Confirm Spring resolves all constructor dependencies successfully.
Step 7: Investigate Circular Dependencies
If application startup now reveals dependency cycles, investigate the design instead of automatically changing back to field injection.
Step 8: Keep Refactoring Focused
Do not simultaneously rewrite unrelated business logic unless necessary.
This makes code review safer and reduces regression risk.
21. Best Practices
Use Constructor Injection for Required Collaborators
Repositories, clients, services, validators, and mappers required for normal behavior should generally be constructor dependencies.
Use Final Fields
Example:
private final CustomerRepository customerRepository;This communicates that the dependency is fixed after construction.
Keep Constructors Simple
A constructor should normally:
- Receive dependencies
- Validate them when appropriate
- Assign fields
Avoid performing business processing.
Depend on the Correct Abstraction
If several implementations may exist, inject the interface that represents the business capability.
Treat Large Constructors as Design Feedback
A large constructor may indicate that the service needs decomposition.
Do not hide the problem by changing injection style.
Keep Required and Optional Dependencies Clear
If a dependency is essential, do not model it as optional.
Make Unit Tests Instantiate the Class Directly
For ordinary service logic:
new OrderService(repository, paymentClient);is preferable to loading Spring purely for dependency injection.
22. Practices to Avoid
Field Injection
@Autowired
private UserRepository userRepository;Why avoid it:
- Hidden dependency
- Mutable field
- Harder isolated testing
- Invalid manual construction possible
Static Service Locator
PaymentService paymentService =
ApplicationContextProvider.getBean(PaymentService.class);Why avoid it:
- Dependencies become invisible
- Strong framework coupling
- Harder testing
- Global access
Manual Dependency Construction
private final PaymentClient paymentClient = new PaymentClient();Why avoid it:
- Tight coupling
- Difficult mocking
- Configuration may be bypassed
- Lifecycle management may be bypassed
Setter Injection for Mandatory Dependencies
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}If the dependency is required, the object can temporarily exist without it.
Huge Constructors Without Investigation
Do not "solve" a constructor with twelve dependencies by moving them back to fields.
The dependency count is exposing a potential responsibility problem.
Heavy Constructor Logic
Avoid:
public ProductService(ProductRepository repository) {
this.products = repository.findAll();
}Database access during construction can make application startup fragile.
23. Code Review Checklist
When reviewing Constructor Injection, ask:
- Are all mandatory dependencies passed through the constructor?
- Are injected dependency fields declared
finalwhere appropriate? - Is field injection being used unnecessarily?
- Can this class be instantiated safely outside the Spring container?
- Does the constructor clearly expose all collaborators used by the class?
- Is
@Autowiredunnecessary because the class has only one constructor? - Is the class manually constructing a dependency that should be injected?
- Does the class depend on an implementation where an abstraction would be more appropriate?
- Are multiple implementations creating bean-resolution ambiguity?
- Is
@Qualifierbeing used only where necessary? - Does the constructor contain database or external API calls?
- Does the constructor perform business logic?
- Does the constructor have too many dependencies?
- Is the large dependency count indicating multiple responsibilities?
- Are any dependencies nullable even though they are mandatory?
- Can unit tests create this class without loading the Spring context?
- Is there a circular dependency between services?
- Is a circular dependency being hidden instead of redesigned?
- Are configuration values represented clearly rather than injected as unrelated primitive values?
- Will this refactoring preserve existing business behavior?
24. Common Pull Request Review Comments
OrderRepositorylooks like a mandatory dependency. Could we inject it through the constructor and make the fieldfinal?
- We can remove
@Autowiredhere because this Spring component has a single constructor.
- Please avoid creating
PaymentClientwithnewinside this service. Inject the client so its configuration and lifecycle remain managed externally.
- This service now has nine constructor dependencies. Could we review whether it has accumulated more than one responsibility?
NotificationSenderhas multiple implementations. Please make the intended bean selection explicit rather than relying on ambiguous injection.
- This repository call is being executed from the constructor. Could we move runtime data loading outside object construction?
- The service currently depends directly on
StripePaymentGateway. Would depending on thePaymentGatewayabstraction better represent what this service actually needs?
- This dependency appears mandatory, so setter injection allows an invalid intermediate state. Constructor Injection would make that requirement explicit.
- Could we keep these injected fields
final? They should not need to change after the service is constructed.
- Please avoid resolving this service through the application context from inside business code. Passing it as a constructor dependency would make the relationship explicit and easier to test.
25. Code Review Exercise
Review the following service as if it appeared in a Pull Request.
Identify:
- Dependency design problems
- Code smells
- Maintainability risks
- Testing difficulties
- Production risks
- Appropriate improvements
Do not focus on formatting.
@Service
public class RefundService {
@Autowired
private RefundRepository refundRepository;
@Autowired
private PaymentGateway paymentGateway;
private NotificationService notificationService;
private final AuditService auditService =
new AuditService();
@Autowired
public void setNotificationService(
NotificationService notificationService) {
this.notificationService = notificationService;
}
public RefundService() {
System.out.println("RefundService created");
}
public RefundResult refund(
Long paymentId,
BigDecimal amount) {
Payment payment =
refundRepository.findPayment(paymentId);
RefundResult result =
paymentGateway.refund(payment.getExternalId(), amount);
auditService.recordRefund(paymentId, amount);
if (result.isSuccessful()) {
notificationService.sendRefundNotification(
payment.getCustomerId());
}
return result;
}
}Learner Task
Review the code and determine:
- Which dependencies should be constructor-injected?
- Which fields should be
final? - Why is
new AuditService()questionable? - Why is setter injection unnecessary here?
- What testing problems exist?
- What object-validity problems exist?
- What should the improved constructor look like?
26. Exercise Solution
The service contains four dependencies:
RefundRepositoryPaymentGatewayNotificationServiceAuditService
All appear necessary for the refund workflow.
Issue 1: Field Injection
@Autowired
private RefundRepository refundRepository;and:
@Autowired
private PaymentGateway paymentGateway;hide required dependencies.
Issue 2: Setter Injection for a Required Dependency
@Autowired
public void setNotificationService(...) {
}If notifications are part of the normal workflow, this dependency should be established when the object is created.
Issue 3: Manual Dependency Creation
private final AuditService auditService =
new AuditService();RefundService is directly responsible for constructing another service.
This can:
- Bypass Spring configuration
- Bypass decorators or proxies
- Make mocking difficult
- Create tight coupling
Issue 4: Public No-Argument Constructor
public RefundService() {
}This allows the object to be created without its required dependencies.
Issue 5: Mutable References
Several dependencies cannot be declared final.
Issue 6: Difficult Unit Testing
Testing requires injecting private fields, invoking setters, or starting Spring.
Improved Code
@Service
public class RefundService {
private final RefundRepository refundRepository;
private final PaymentGateway paymentGateway;
private final NotificationService notificationService;
private final AuditService auditService;
public RefundService(
RefundRepository refundRepository,
PaymentGateway paymentGateway,
NotificationService notificationService,
AuditService auditService) {
this.refundRepository = refundRepository;
this.paymentGateway = paymentGateway;
this.notificationService = notificationService;
this.auditService = auditService;
}
public RefundResult refund(
Long paymentId,
BigDecimal amount) {
Payment payment =
refundRepository.findPayment(paymentId);
RefundResult result =
paymentGateway.refund(
payment.getExternalId(),
amount);
auditService.recordRefund(paymentId, amount);
if (result.isSuccessful()) {
notificationService.sendRefundNotification(
payment.getCustomerId());
}
return result;
}
}Why This Is Better
All dependencies are now visible in one place.
The fields are immutable references.
The service cannot normally be created without required collaborators.
Unit tests can easily construct it:
RefundService refundService = new RefundService(
refundRepository,
paymentGateway,
notificationService,
auditService);No reflection or Spring context is required.
Spring remains responsible for constructing infrastructure and service beans.
27. Interview Perspective
Constructor Injection frequently appears in:
- Java interviews
- Spring Boot interviews
- Senior developer interviews
- Architecture discussions
- Pull Request exercises
- SOLID principle discussions
Interviewers are usually more interested in the reasoning than the syntax.
A weak answer is:
Constructor Injection is better because Spring recommends it.
A stronger answer explains:
- Required dependencies become explicit.
- Dependencies can be final.
- Objects can be instantiated directly in unit tests.
- Invalid partially initialized objects are harder to create.
- Dependency count becomes visible.
- Circular dependencies are exposed.
- Business classes become less dependent on reflection-based field injection.
- A large constructor can reveal responsibility problems.
Scenario-Based Discussion
An interviewer may ask:
Your Spring service contains eight
@Autowiredfields. What would you change?
A strong response would be:
- Convert required collaborators to constructor dependencies.
- Declare them
final. - Check whether all eight are genuinely required.
- Review whether the class has too many responsibilities.
- Avoid mechanically wrapping dependencies into another object merely to make the constructor shorter.
- Write isolated tests using mocks.
28. Interview Questions and Answers
Basic Question
Question: What is Constructor Injection in Spring?
Answer:
Constructor Injection means providing a class's dependencies through its constructor.
Example:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}Spring creates UserRepository and supplies it when constructing UserService.
It makes required dependencies explicit and allows the field to remain final.
Intermediate Question
Question: Why is Constructor Injection generally preferred over field injection?
Answer:
Constructor Injection provides several practical advantages:
- Dependencies are explicit.
- Required dependencies are available when the object is created.
- Dependency fields can be
final. - Unit tests can instantiate the class directly.
- The class relies less on reflection-based field injection.
- Large dependency counts become visible during code review.
Field injection may be shorter syntactically, but it hides dependency requirements.
Advanced Question
Question: A service has twelve constructor parameters after you replace field injection. Is Constructor Injection causing a design problem?
Answer:
No.
Constructor Injection is exposing an existing design problem rather than creating it.
Twelve dependencies may indicate that the service:
- Has multiple responsibilities
- Coordinates too many unrelated systems
- Contains business logic belonging elsewhere
- Has become an oversized application service
The correct response is to review the class design.
Moving the dependencies back to fields only hides the coupling.
However, a high dependency count is a design signal rather than automatic proof that the class must be split. The actual responsibilities should be evaluated before refactoring.
Scenario-Based Question
Question: You convert a field-injected Spring service to Constructor Injection and the application reports a circular dependency. What should you do?
Answer:
Investigate the architecture.
For example:
OrderService -> PaymentService -> OrderServicemay indicate that both services contain responsibilities that are too tightly coupled.
Possible solutions include:
- Moving shared behavior into another component
- Changing the direction of orchestration
- Publishing an event where loose coupling is appropriate
- Reconsidering service boundaries
Switching back to field injection simply to avoid seeing the circular relationship is usually not the correct architectural fix.
Code-Review Question
Question: What would you comment on this code?
@Service
public class ReportService {
@Autowired
private ReportRepository reportRepository;
}Answer:
A suitable review comment would be:
ReportRepositoryappears to be a required collaborator. Could we inject it through the constructor and keep the fieldfinal? That makes the dependency explicit and allows this service to be instantiated directly in unit tests.
Improved version:
@Service
public class ReportService {
private final ReportRepository reportRepository;
public ReportService(ReportRepository reportRepository) {
this.reportRepository = reportRepository;
}
}Real-Project Question
Question: What practical benefit have you seen from Constructor Injection in large Spring Boot applications?
Answer:
One important benefit is architectural visibility.
When a service constructor starts requiring many repositories, clients, and other services, the dependency list immediately becomes visible during code review.
That often triggers useful discussions about:
- Service responsibilities
- Coupling
- Orchestration boundaries
- Test complexity
- Component ownership
Constructor Injection also makes ordinary unit testing easier because the service can be instantiated directly with mocks without loading the Spring application context.
29. Quick Rule to Remember
If a class cannot work correctly without a dependency, require that dependency through the constructor.
30. Final Takeaway
Constructor Injection makes dependency requirements part of the class design rather than hidden framework configuration.
A Java developer should remember:
- Required collaborators belong in the constructor.
- Dependency fields should normally be
final. - Spring does not require
@Autowiredon a single constructor. - Services should not manually construct infrastructure dependencies.
- Constructors should assign dependencies, not execute business operations.
- Large constructors should trigger a responsibility review.
During Pull Request review, check whether:
- Dependencies are explicit.
- Required dependencies can never be forgotten during normal object construction.
- Dependencies are immutable references where appropriate.
- The class can be tested without starting Spring.
- The class is manually creating objects that should be injected.
- The constructor has grown large enough to indicate excessive responsibility.
- Circular dependencies reveal architectural coupling.
Avoid production designs where dependency relationships are hidden through:
- Field injection
- Service locators
- Static access
- Manual construction of managed services
- Setter injection for mandatory collaborators
Constructor Injection does not automatically make a class well designed, but it makes the design much easier to see, review, test, and improve.