Building the Spring Boot Application
Dependency Injection
Our Order Management Backend is still at the beginning of implementation.
The application has been bootstrapped, and we understand its project structure.
Soon, real application components will need to collaborate.
For example, Order creation will eventually look conceptually like:
CreateOrderHandler
↓
CreateOrderUseCase
↓
ProductRepository
InventoryRepository
OrderRepository
Payment will look like:
PayOrderHandler
↓
PayOrderUseCase
↓
OrderRepository
PaymentService
This raises an important question:
How does an object receive the collaborators it needs?
That problem leads us to Dependency Injection.
What Is a Dependency?
A class has a dependency when it needs another collaborator to complete its responsibility.
For example:
public class CreateOrderUseCase {
private final OrderRepository orderRepository;
}
CreateOrderUseCase needs OrderRepository to persist or retrieve Order state.
Therefore:
CreateOrderUseCase
↓ depends on
OrderRepository
OrderRepository is a dependency of CreateOrderUseCase.
Dependencies Are Normal
Objects in real software systems collaborate.
For example:
CreateOrderHandler
↓
CreateOrderUseCase
and:
CreateOrderUseCase
↓
ProductRepository
CreateOrderUseCase
↓
InventoryRepository
CreateOrderUseCase
↓
OrderRepository
Having dependencies is not itself a design problem.
The important questions are:
Who creates the dependency?
and:
How does the object obtain it?
The Naive Approach
Suppose we write:
public class CreateOrderUseCase {
private final OrderRepository orderRepository =
new PostgresOrderRepository();
public void execute() {
// ...
}
}
Now CreateOrderUseCase is constructing its own persistence implementation.
That means the UseCase no longer knows only about:
how to coordinate Order creation
It also knows:
which repository implementation exists
how that implementation is constructed
which persistence technology is used
Responsibilities are becoming mixed.
Construction Is a Different Responsibility
The job of:
CreateOrderUseCase
is:
coordinate Order creation
Its job is not:
construct database infrastructure
A better direction is:
Application Composition
↓
constructs collaborators
↓
provides them to UseCases
Then the UseCase can focus on the workflow it owns.
Dependency Injection in One Sentence
Dependency Injection means an object receives the collaborators it needs instead of constructing them internally.
For example:
public class CreateOrderUseCase {
private final OrderRepository orderRepository;
public CreateOrderUseCase(
OrderRepository orderRepository
) {
this.orderRepository =
orderRepository;
}
}
CreateOrderUseCase no longer creates the Repository.
The dependency is supplied from outside.
That is Dependency Injection.
Constructor Injection
When dependencies are supplied through a constructor, we call it:
Constructor Injection
For example:
public CreateOrderUseCase(
OrderRepository orderRepository
) {
this.orderRepository =
orderRepository;
}
The constructor immediately communicates:
CreateOrderUseCase
requires OrderRepository
The dependency is explicit.
Why Explicit Dependencies Matter
Compare two designs.
Hidden Dependency
public class CreateOrderUseCase {
private final OrderRepository repository =
new PostgresOrderRepository();
}
A caller cannot tell from the class's construction contract what infrastructure it creates internally.
Explicit Dependency
public class CreateOrderUseCase {
private final OrderRepository repository;
public CreateOrderUseCase(
OrderRepository repository
) {
this.repository = repository;
}
}
Now the dependency is part of the object's construction contract.
This improves:
clarity
testability
replaceability
configuration
architecture visibility
Dependency Injection Is Not Spring-Specific
Dependency Injection is a general software-design technique.
We can perform DI using plain Java.
For example:
OrderRepository orderRepository =
new PostgresOrderRepository();
CreateOrderUseCase createOrderUseCase =
new CreateOrderUseCase(
orderRepository
);
The caller:
- creates the Repository
- creates the UseCase
- supplies the Repository to the UseCase
That is Dependency Injection.
Spring later automates the composition.
Manual Dependency Injection
Imagine:
public interface ProductRepository {
// persistence operations
}
and:
public class CreateProductUseCase {
private final ProductRepository productRepository;
public CreateProductUseCase(
ProductRepository productRepository
) {
this.productRepository =
productRepository;
}
}
Manual composition might be:
ProductRepository productRepository =
new PostgresProductRepository();
CreateProductUseCase createProductUseCase =
new CreateProductUseCase(
productRepository
);
There is nothing wrong with this.
Spring becomes especially useful as the application's dependency graph becomes larger.
Our Application Dependency Flow
Our preferred internal application flow is:
Handler
↓
UseCase
↓
Repository
When an external system participates:
Handler
↓
UseCase
├── Repository
└── External Service
For example:
PayOrderHandler
↓
PayOrderUseCase
├── OrderRepository
└── PaymentService
PaymentService represents an external capability.
We do not use Service as the generic name for internal application workflows.
What Does a Handler Do?
A Handler receives input at an application boundary and invokes the appropriate UseCase.
Later, in our HTTP API:
HTTP Request
↓
Handler
↓
UseCase
A Handler may be responsible for:
receiving input
transport-level validation and mapping
obtaining authenticated context
invoking a UseCase
mapping the result
The business workflow should not live inside the Handler.
What Does a UseCase Do?
A UseCase coordinates one meaningful application operation.
Examples include:
CreateOrderUseCase
CancelOrderUseCase
PayOrderUseCase
CreateProductUseCase
SetInventoryUseCase
A UseCase may:
load required state
coordinate Domain behaviour
apply ownership and workflow rules
call Repositories
call external Services when required
persist resulting state
It represents the application operation.
What Does a Repository Do?
A Repository provides access to application-owned persistent state.
Examples:
ProductRepository
InventoryRepository
OrderRepository
Repository responsibilities include:
load data
save data
execute persistence-specific queries
hide database mechanics from UseCases
The UseCase should not contain:
raw SQL
EntityManager operations
JPA query mechanics
PostgreSQL connection handling
Those belong below the Repository boundary.
What Does Service Mean in Our Project?
In this course, Service is reserved for external or third-party capabilities.
For example:
PaymentService
and potentially other external integration boundaries when they actually exist.
Internal application workflows use names such as:
CreateOrderUseCase
not:
OrderService
This keeps our vocabulary aligned with responsibility.
Dependency Injection Supports This Architecture
Consider:
CreateOrderHandler
↓
CreateOrderUseCase
↓
Repositories
Constructor injection can make every collaboration explicit.
For example:
public class CreateOrderHandler {
private final CreateOrderUseCase createOrderUseCase;
public CreateOrderHandler(
CreateOrderUseCase createOrderUseCase
) {
this.createOrderUseCase =
createOrderUseCase;
}
}
And:
public class CreateOrderUseCase {
private final ProductRepository productRepository;
private final InventoryRepository inventoryRepository;
private final OrderRepository orderRepository;
public CreateOrderUseCase(
ProductRepository productRepository,
InventoryRepository inventoryRepository,
OrderRepository orderRepository
) {
this.productRepository =
productRepository;
this.inventoryRepository =
inventoryRepository;
this.orderRepository =
orderRepository;
}
}
The application's dependency relationships are visible directly in the constructors.
Why Constructor Injection Is Our Default
Spring supports several ways to supply dependencies:
Constructor Injection
Field Injection
Setter Injection
For required collaborators, our default is:
Constructor Injection
There are several reasons.
Reason 1 — Dependencies Are Visible
Consider:
public PayOrderUseCase(
OrderRepository orderRepository,
PaymentService paymentService
) {
}
An engineer opening the class immediately knows:
PayOrderUseCase requires
Order persistence
and
an external Payment capability
The architecture is visible.
Reason 2 — Required Collaborators Stay Required
If a UseCase cannot operate without:
OrderRepository
then it should not be possible to create a correctly constructed UseCase without one.
The constructor communicates that requirement.
We do not want partially configured application components.
Reason 3 — Dependencies Can Be final
For example:
private final OrderRepository orderRepository;
The reference is assigned during construction and does not need to change afterward.
That makes the component easier to reason about.
Reason 4 — Tests Stay Simple
Suppose:
public class CancelOrderUseCase {
private final OrderRepository orderRepository;
public CancelOrderUseCase(
OrderRepository orderRepository
) {
this.orderRepository =
orderRepository;
}
}
A test can construct it directly:
OrderRepository repository =
new FakeOrderRepository();
CancelOrderUseCase useCase =
new CancelOrderUseCase(
repository
);
No Spring Application Context is required just to test the application behaviour.
Field Injection
Spring code sometimes uses:
@Autowired
private OrderRepository orderRepository;
This is:
Field Injection
It is shorter.
But it is not our preferred style for required dependencies.
Why We Avoid Field Injection
Field injection tends to:
hide construction requirements
make plain Java construction harder
make dependencies less obvious
encourage framework-dependent tests
Constructor injection keeps the dependency graph more visible.
Setter Injection
Setter injection might look like:
public void setOrderRepository(
OrderRepository orderRepository
) {
this.orderRepository =
orderRepository;
}
Setter injection can make sense when a collaborator is genuinely:
optional
replaceable after construction
Our core Repositories and external Payment dependency are not optional in that sense.
Constructor injection communicates their role more accurately.
Modern Spring and @Autowired
Suppose a Spring-managed class has a single constructor:
public CreateOrderHandler(
CreateOrderUseCase createOrderUseCase
) {
this.createOrderUseCase =
createOrderUseCase;
}
Modern Spring can normally use that constructor automatically.
We usually do not need to add:
@Autowired
to a single required constructor.
This keeps the class readable as ordinary Java.
Dependency Injection Does Not Require Interfaces Everywhere
Using DI does not mean every dependency needs an interface.
For example:
public class CreateOrderHandler {
private final CreateOrderUseCase createOrderUseCase;
}
CreateOrderUseCase can be a concrete class.
That is completely reasonable.
We do not need:
CreateOrderUseCase
CreateOrderUseCaseImpl
simply because DI is being used.
Avoid Interface + Impl by Habit
We do not automatically create:
CreateOrderUseCase
CreateOrderUseCaseImpl
or other interface/implementation pairs without a meaningful abstraction.
An interface should represent a real boundary.
It should not exist merely as architectural decoration.
Where an Interface Makes Sense
An external system boundary is a strong example.
Our application wants the capability:
process Payment
without depending directly on one provider protocol.
An application-facing boundary might be:
public interface PaymentService {
PaymentResult pay(
PaymentRequest request
);
}
A provider implementation might be:
public class ProviderPaymentService
implements PaymentService {
@Override
public PaymentResult pay(
PaymentRequest request
) {
// provider-specific integration
return null;
}
}
Then:
public class PayOrderUseCase {
private final PaymentService paymentService;
public PayOrderUseCase(
PaymentService paymentService
) {
this.paymentService =
paymentService;
}
}
The UseCase depends on the application-facing Payment capability rather than the provider protocol.
External Services and UseCases Are Different
PayOrderUseCase owns:
application workflow coordination
PaymentService owns:
communication with the external
Payment capability
Therefore:
PayOrderUseCase
↓
PaymentService
is a meaningful relationship.
But:
CreateOrderService
for our internal Order-creation workflow would conflict with our architecture vocabulary.
That workflow is:
CreateOrderUseCase
Domain Objects Are Different
Suppose we create an Order:
Order order =
Order.create(
orderId,
customerId,
items,
createdAt
);
Order represents request-specific business state.
It is not a long-lived application collaborator that Spring should inject into a UseCase.
Each request creates or reconstructs a different Order.
Component Dependency vs Business State
A UseCase constructor contains stable collaborators such as:
Repositories
External Services
Clock
Operation input contains request-specific information such as:
CustomerId
OrderId
Command
Domain objects include:
Order
OrderItem
Inventory
Product
These concepts have different roles.
Do not treat all objects as injectable components.
Do Not Inject Domain Entities
Bad:
public class CreateOrderUseCase {
private final Order order;
public CreateOrderUseCase(
Order order
) {
this.order = order;
}
}
Which Order?
Every workflow invocation creates or loads different business state.
A UseCase should operate on request-specific Domain objects rather than hold one injected Entity.
Spring Manages Application Components
Conceptually, Spring may eventually manage:
Handlers
UseCases
Repository implementations
Security components
External Service implementations
Configuration components
But individual:
Product
Inventory
Order
OrderItem
instances remain normal business objects created or reconstituted during application workflows.
Spring manages application composition.
The Domain represents business state and behaviour.
Example: Handler Injection
A future Handler may look like:
public class CreateOrderHandler {
private final CreateOrderUseCase createOrderUseCase;
public CreateOrderHandler(
CreateOrderUseCase createOrderUseCase
) {
this.createOrderUseCase =
createOrderUseCase;
}
public OrderResponse handle(
CreateOrderRequest request
) {
// map request
// invoke UseCase
// map result
return null;
}
}
The Handler does not construct the UseCase.
Composition supplies it.
Example: UseCase Injection
public class CreateOrderUseCase {
private final ProductRepository productRepository;
private final InventoryRepository inventoryRepository;
private final OrderRepository orderRepository;
public CreateOrderUseCase(
ProductRepository productRepository,
InventoryRepository inventoryRepository,
OrderRepository orderRepository
) {
this.productRepository =
productRepository;
this.inventoryRepository =
inventoryRepository;
this.orderRepository =
orderRepository;
}
}
The UseCase does not construct database implementations.
It receives the persistence capabilities it requires.
Why a Handler Should Not Construct Its UseCase
Bad:
public class CreateOrderHandler {
private final CreateOrderUseCase useCase =
new CreateOrderUseCase(
new PostgresProductRepository(),
new PostgresInventoryRepository(),
new PostgresOrderRepository()
);
}
Now the Handler knows:
UseCase construction
Repository construction
persistence composition
Its HTTP/application-boundary responsibility is polluted.
Why a UseCase Should Not Construct Repositories
Bad:
public class CreateOrderUseCase {
private final OrderRepository orderRepository =
new PostgresOrderRepository();
}
Now application workflow code knows a concrete persistence implementation.
If the persistence implementation changes, the UseCase changes unnecessarily.
Dependency Injection removes that construction responsibility from the UseCase.
DI and Repository Boundaries
The UseCase should depend on meaningful persistence capabilities.
Conceptually:
CreateOrderUseCase
↓
ProductRepository
InventoryRepository
OrderRepository
Later, those boundaries may expose operations such as:
find Product
consume Inventory if available
save Order
find Customer-owned Order
Database mechanics remain inside persistence.
The UseCase should not create or manipulate:
EntityManager
DataSource
database connections
directly.
DI and Third-Party Services
Bad:
public class PayOrderUseCase {
private final ProviderPaymentService paymentService =
new ProviderPaymentService(
"https://provider.example.com",
"secret"
);
}
Now the UseCase knows:
provider implementation
provider URL
credentials
construction details
A better design is:
public class PayOrderUseCase {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
public PayOrderUseCase(
OrderRepository orderRepository,
PaymentService paymentService
) {
this.orderRepository =
orderRepository;
this.paymentService =
paymentService;
}
}
Infrastructure composition provides the actual provider implementation.
Dependency Injection and Configuration
The provider implementation may need:
base URL
API credentials
timeout
Those values come from application configuration.
Conceptually:
Configuration
↓
Provider Payment Integration
↓
PaymentService
↓
PayOrderUseCase
The UseCase should not read environment variables or provider configuration itself.
Configuration is a separate responsibility.
DI Does Not Mean “Never Use new”
This is an important misconception.
We will still use new or factory methods for business state.
For example:
OrderItem orderItem =
new OrderItem(
productId,
quantity,
unitPriceCents
);
That is normal.
Likewise, creating an Order as part of the workflow is normal.
What we want to avoid is a high-level application component hard-wiring infrastructure collaborators.
A Useful Question About new
Whenever you see:
new Something(...)
ask:
Am I creating business state, or am I hard-wiring a stable collaborator?
Business state:
new OrderItem(...)
Normal.
Infrastructure collaborator inside a UseCase:
new ProviderPaymentService(...)
Potential warning sign.
DI and Testability
Suppose we want to test:
PayOrderUseCase
Its constructor is:
public PayOrderUseCase(
OrderRepository orderRepository,
PaymentService paymentService
) {
this.orderRepository =
orderRepository;
this.paymentService =
paymentService;
}
A test can supply controlled collaborators:
OrderRepository orderRepository =
new FakeOrderRepository();
PaymentService paymentService =
new SuccessfulPaymentService();
PayOrderUseCase useCase =
new PayOrderUseCase(
orderRepository,
paymentService
);
No real PostgreSQL.
No real Payment provider.
The workflow can be tested in isolation from those infrastructure systems.
A Fake External Service
A test implementation might look conceptually like:
public class SuccessfulPaymentService
implements PaymentService {
@Override
public PaymentResult pay(
PaymentRequest request
) {
return PaymentResult.success();
}
}
A different test double could simulate failure or ambiguity.
Test doubles allow deterministic workflow testing.
They are not production provider implementations.
Mockito Can Also Supply Dependencies
Later, a test may use mocks:
OrderRepository repository =
mock(OrderRepository.class);
PaymentService paymentService =
mock(PaymentService.class);
PayOrderUseCase useCase =
new PayOrderUseCase(
repository,
paymentService
);
Constructor injection makes this straightforward.
But Mockito is not the reason we use DI.
The deeper reason is:
clear composition and responsibility boundaries
Testing is one benefit.
Do Not Mock Domain Objects by Default
Objects such as:
Order
OrderItem
Inventory
Product
usually represent business behaviour we actually want to exercise in tests.
Mocks and fakes are often more useful at boundaries such as:
Repositories
external Services
where infrastructure or side effects exist.
Dependencies Make Architecture Visible
Consider:
public CancelOrderUseCase(
OrderRepository orderRepository,
InventoryRepository inventoryRepository
) {
}
The constructor itself tells us:
Cancellation coordinates
Order state
and
Inventory state
That matches our technical design.
Constructor signatures can act as lightweight architecture documentation.
Too Many Dependencies Can Reveal a Design Problem
Suppose we find:
public SomeOrderUseCase(
OrderRepository orderRepository,
ProductRepository productRepository,
InventoryRepository inventoryRepository,
PaymentService paymentService,
NotificationService notificationService,
IdentityService identityService,
AnotherRepository anotherRepository
) {
}
That may be valid in some system.
But it should trigger a question:
Does this one UseCase genuinely own all of these responsibilities?
DI makes coupling visible.
Do not hide the signal with field injection.
Constructor Size Is Design Feedback
A constructor with several cohesive dependencies is normal.
For example:
ProductRepository
InventoryRepository
OrderRepository
Clock
can all make sense for Order creation.
There is no useful rule such as:
A constructor may have at most three dependencies.
The important question is:
Are these dependencies coherent
with this component's responsibility?
Avoid Circular Dependencies
Imagine:
OrderUseCase
↓
InventoryUseCase
and:
InventoryUseCase
↓
OrderUseCase
Now the application graph contains a cycle.
Spring may have trouble constructing it.
More importantly, the responsibility design is unclear.
Cross-Capability Coordination Belongs in One Workflow Owner
For cancellation, we do not want:
OrderUseCase
↓
InventoryUseCase
↓
OrderUseCase
Instead:
CancelOrderUseCase
├── OrderRepository
└── InventoryRepository
One UseCase owns the complete cancellation workflow.
This avoids circular application dependencies and gives the operation a clear owner.
Avoid Service Locator Style
A component could technically ask Spring for dependencies manually:
applicationContext.getBean(
OrderRepository.class
);
inside application code.
But now the dependency disappears from the constructor.
The class no longer clearly declares what it needs.
We avoid this pattern.
Do Not Inject ApplicationContext Into UseCases
Bad:
public class CreateOrderUseCase {
private final ApplicationContext context;
}
and later:
context.getBean(...);
This turns Spring into a global dependency registry.
Prefer:
public CreateOrderUseCase(
ProductRepository productRepository,
InventoryRepository inventoryRepository,
OrderRepository orderRepository,
Clock clock
) {
}
The dependency graph remains explicit.
Avoid Static Global Dependencies
Bad:
PaymentServiceHolder
.get()
.pay(...);
or:
GlobalRepositories
.orderRepository();
These patterns hide collaboration and introduce global state.
Explicit constructor dependencies are easier to understand and test.
Request Data Is Not a Constructor Dependency
A UseCase constructor may be:
public CreateOrderUseCase(
ProductRepository productRepository,
InventoryRepository inventoryRepository,
OrderRepository orderRepository,
Clock clock
) {
}
But request-specific input belongs in the operation:
public Order execute(
CustomerId customerId,
CreateOrderCommand command
) {
// ...
}
This distinction matters.
Constructor dependencies are:
stable collaborators
Method arguments are:
request-specific input
Keep Request-Specific State Out of Fields
Bad:
public class CreateOrderUseCase {
private CustomerId currentCustomer;
public void execute(
CustomerId customerId
) {
this.currentCustomer =
customerId;
}
}
Spring-managed application components are typically shared and may serve concurrent requests.
Mutable request-specific fields can therefore create concurrency bugs.
Prefer:
public Order execute(
CustomerId customerId,
CreateOrderCommand command
) {
// request-specific local state
}
Application Components Should Generally Be Stateless
CreateOrderUseCase may hold:
ProductRepository
InventoryRepository
OrderRepository
Clock
because these are stable collaborators.
It should not hold mutable fields such as:
current Customer
current Order
current request
across executions.
Stateless application components are easier to reason about under concurrent requests.
DI Does Not Remove Coupling
Suppose:
public PayOrderUseCase(
ProviderPaymentService paymentService
) {
}
The dependency is injected.
But the UseCase still depends directly on one provider-specific implementation.
DI has not removed that architectural coupling.
If provider isolation matters, the better boundary is:
public PayOrderUseCase(
PaymentService paymentService
) {
}
What DI Actually Does
A more accurate statement is:
Dependency Injection does not eliminate coupling. It makes collaborators externally supplied, explicit, configurable, testable, and replaceable.
We still need good architectural boundaries.
DI cannot choose them for us.
Not Every Dependency Needs Abstraction
A Handler depending directly on:
CreateOrderUseCase
is fine.
We do not need:
CreateOrderUseCaseInterface
because there is no meaningful alternate boundary to protect.
Internal application components can depend directly on one another when that dependency matches the architecture.
External Systems Deserve Stronger Boundaries
A Payment provider introduces:
network failure
provider protocol
credentials
request/response mapping
timeouts
provider-specific change
That makes an application-facing:
PaymentService
boundary valuable.
External systems are fundamentally different from direct in-process UseCase collaboration.
Repository Boundaries Are Meaningful Too
A UseCase should be able to express:
find Order
save Order
consume Inventory
load Product
without knowing:
SQL
JPA query syntax
EntityManager
PostgreSQL driver behaviour
The Repository boundary hides those persistence mechanics while exposing meaningful persistence capabilities.
Dependency Injection and Transactions Are Different Concerns
This constructor:
CreateOrderUseCase(
ProductRepository productRepository,
InventoryRepository inventoryRepository,
OrderRepository orderRepository
)
does not make Order creation transactional.
DI answers:
Which collaborators does this component receive?
Transaction management answers:
Which local persistence operations must commit or roll back together?
They are separate concerns.
Dependency Injection and Error Handling Are Different
Injecting:
PaymentService
does not answer:
What happens when the provider times out?
That belongs to Payment workflow and integration design.
Dependency Injection manages composition.
It does not define failure semantics.
Dependency Injection and Security Are Different
Authenticated:
CustomerId
may flow:
Handler
↓
UseCase
as request context.
That is not Dependency Injection.
DI may compose stable security infrastructure components.
But request identity is operation-specific data.
These are different concerns.
Spring's Role
Eventually Spring will know which application components exist.
Conceptually:
Spring
↓
creates Repository implementation
↓
creates UseCase(repository)
↓
creates Handler(useCase)
For a Payment workflow:
Spring
├── creates OrderRepository implementation
├── creates PaymentService implementation
↓
creates PayOrderUseCase(...)
↓
creates PayOrderHandler(...)
Spring builds the application object graph.
The Dependency Graph
Future Order creation:
CreateOrderHandler
↓
CreateOrderUseCase
├── ProductRepository
├── InventoryRepository
├── OrderRepository
└── Clock
Payment:
PayOrderHandler
↓
PayOrderUseCase
├── OrderRepository
└── PaymentService
This graph describes real application collaboration.
Spring Wiring Failure
Suppose:
public CreateOrderUseCase(
OrderRepository orderRepository
) {
}
but Spring cannot find a managed implementation or Bean matching:
OrderRepository
Application startup may fail.
That is useful.
It tells us immediately:
required dependency is missing
rather than allowing a structurally incomplete application to start successfully.
Fail Fast
Required application wiring should normally fail early when incomplete.
A backend missing one of its required core collaborators should not pretend to be healthy.
Spring's dependency resolution can expose composition mistakes during startup.
Multiple Implementations
Suppose later:
PaymentService
has two implementations:
ProviderAPaymentService
ProviderBPaymentService
Spring then needs enough configuration to determine which implementation should be used.
Spring provides mechanisms for resolving such ambiguity.
But our current v1 has one provider.
We do not introduce multi-provider complexity before the requirement exists.
Do Not Add Fake Production Implementations to Demonstrate DI
We do not add:
DemoPaymentService
FakePaymentService
MockPaymentService
to production code merely to demonstrate polymorphism.
Test doubles belong in tests.
Production abstractions should correspond to real production boundaries.
Spring Components vs Domain Objects
A useful distinction is:
Spring-managed application components
Handlers
UseCases
Repository implementations
External Service adapters
Security components
configuration components
Ordinary Domain objects
Product
Inventory
Order
OrderItem
Spring composes the application.
Domain objects represent business state and behaviour.
Cancellation Example
Handler:
public class CancelOrderHandler {
private final CancelOrderUseCase cancelOrderUseCase;
public CancelOrderHandler(
CancelOrderUseCase cancelOrderUseCase
) {
this.cancelOrderUseCase =
cancelOrderUseCase;
}
}
UseCase:
public class CancelOrderUseCase {
private final OrderRepository orderRepository;
private final InventoryRepository inventoryRepository;
public CancelOrderUseCase(
OrderRepository orderRepository,
InventoryRepository inventoryRepository
) {
this.orderRepository =
orderRepository;
this.inventoryRepository =
inventoryRepository;
}
}
The constructor communicates that cancellation coordinates both Order and Inventory persistence.
Domain Behaviour Remains in the Domain
DI does not mean every business rule moves into a UseCase.
For example:
order.cancel();
remains better than:
order.setStatus(
OrderStatus.CANCELLED
);
because the Order owns the validity of its lifecycle transition.
The UseCase coordinates:
load Customer-owned Order
invoke Order.cancel()
restore Inventory
persist atomically
Domain objects protect their own local invariants.
Avoid Injectable “Rule Services” Without Need
A common tendency is to create:
OrderCancellationService
OrderTotalService
InventoryValidationService
for every piece of behaviour.
But if the behaviour naturally belongs to:
Order
or:
Inventory
keep it there.
Dependency Injection should not turn an object-oriented Domain model into a collection of procedural Service classes.
Handler → UseCase → Repository in Practice
Typical request path:
HTTP Request
↓
CreateOrderHandler
↓
CreateOrderUseCase
↓
Repositories
↓
PostgreSQL
With an external system:
HTTP Request
↓
PayOrderHandler
↓
PayOrderUseCase
├── OrderRepository
└── PaymentService
↓
Payment Provider
This remains the architectural vocabulary we use throughout the course.
Constructor Dependencies as Architecture Documentation
Consider:
public PayOrderUseCase(
OrderRepository orderRepository,
PaymentService paymentService
) {
}
Without reading the implementation, we already know:
PayOrderUseCase coordinates
local Order persistence
with an external Payment capability
Clear constructors make reviews easier.
A Large Constructor Is a Signal
Suppose:
public SomeUseCase(
ProductRepository productRepository,
InventoryRepository inventoryRepository,
OrderRepository orderRepository,
PaymentService paymentService,
NotificationService notificationService,
IdentityService identityService,
AnotherRepository anotherRepository
) {
}
This may occasionally be correct.
But it should prompt:
Does one application operation genuinely own all of these collaborations?
Do not immediately hide the constructor using field injection.
Use the dependency list as design feedback.
Avoid Generic UseCases
We do not want:
OrderUseCase
that handles:
create
cancel
pay
history
admin operations
Prefer operation-specific UseCases:
CreateOrderUseCase
CancelOrderUseCase
PayOrderUseCase
GetOrderHistoryUseCase
This gives each workflow a clear responsibility and usually keeps its dependency set focused.
Handler Naming
Handlers should correspond to clear application entry points.
Examples:
CreateOrderHandler
CancelOrderHandler
PayOrderHandler
When Spring MVC arrives, those classes may carry framework annotations or be wrapped by very thin HTTP adapters.
The architectural relationship remains:
Handler
↓
UseCase
What About Controllers?
Spring MVC uses:
@RestController
and the wider Spring ecosystem commonly uses the term:
Controller
Our architecture uses:
Handler
for the HTTP/application entry-point responsibility.
When we implement REST APIs, we will choose the concrete Spring structure that best preserves that boundary.
The framework annotation does not change the architectural responsibility.
Spring Annotations Do Not Define Architecture
A class marked:
@Component
is not automatically well designed.
Likewise:
@Service
does not automatically mean:
this is where business logic belongs
Architecture comes from responsibility.
Our project vocabulary remains:
Handler
→ incoming boundary
UseCase
→ application workflow
Repository
→ persistence boundary
Service
→ external capability
Dependency Injection Review Checklist
When introducing an application component, ask:
What stable collaborators
does this component require?
Are they explicit constructor dependencies?
Is this component a Handler,
UseCase, Repository,
or external Service?
Am I constructing infrastructure
inside application logic?
Am I injecting request-specific
business state by mistake?
Does every dependency belong
to this responsibility?
Am I introducing an interface
without a real boundary?
Does the constructor suggest
too much responsibility?
Have I created a circular dependency?
Am I using static/global/container lookup
instead of explicit dependencies?
These questions keep DI aligned with architecture.
Common Mistake 1 — Field Injection
Avoid:
@Autowired
private OrderRepository orderRepository;
for required collaborators.
Prefer constructor injection.
Common Mistake 2 — Constructing Infrastructure Inside a UseCase
Avoid:
new PostgresOrderRepository();
inside:
CreateOrderUseCase
The Repository implementation should be provided through application composition.
Common Mistake 3 — Constructing an External Client Inside a UseCase
Avoid:
new ProviderPaymentService(...);
inside:
PayOrderUseCase
Inject the external capability through:
PaymentService
instead.
Common Mistake 4 — Injecting Domain Entities
Avoid:
CreateOrderUseCase(
Order order
)
A specific Order is request/business state.
It is not a shared application collaborator.
Common Mistake 5 — Service Locator
Avoid:
applicationContext.getBean(...);
inside application workflows.
This hides the dependency graph.
Common Mistake 6 — Static Globals
Avoid:
GlobalRepositories.orderRepository();
Global access hides collaboration and complicates testing.
Common Mistake 7 — Circular UseCases
Avoid:
UseCase A
↓
UseCase B
↓
UseCase A
This usually indicates unclear workflow ownership.
Common Mistake 8 — Request State in Component Fields
Avoid:
private OrderId currentOrderId;
inside a shared Spring-managed UseCase.
Keep request data in:
method parameters
local variables
Common Mistake 9 — Interface for Everything
Avoid:
CreateOrderUseCase
CreateOrderUseCaseImpl
without a meaningful reason.
DI does not require interfaces everywhere.
Common Mistake 10 — Moving Domain Behaviour Into Injectable Services
Avoid creating:
OrderStateService
for rules naturally owned by:
Order
UseCases coordinate.
Domain objects protect the local business behaviour that belongs to them.
Dependency Injection and OOP
Our Java and OOP foundation established that objects collaborate.
DI makes those collaborations explicit.
Handler
receives
UseCase
UseCase
receives
Repositories / external Services
rather than each object secretly creating everything it needs.
This supports:
encapsulation
clear responsibility
replaceable boundaries
testability
Encapsulation + DI
Domain:
Order
↓
protects Order lifecycle
Application:
CancelOrderUseCase
↓
coordinates Order + Inventory
Composition:
Spring
↓
provides application collaborators
Each concept has a different responsibility.
DI Is About Composition
Dependency Injection answers:
How does an application component receive the collaborators it needs?
It does not answer:
What business rules exist?
Where is the transaction boundary?
How should Payment timeout work?
How is Inventory concurrency protected?
How is authorization enforced?
Those are separate design problems.
DI is one part of application composition.
What Spring Will Do Next
At a high level:
Spring discovers managed components
↓
creates them
↓
resolves constructor dependencies
↓
builds the application object graph
This creates several new questions:
What exactly is a Spring-managed component?
What is a Bean?
What is the Application Context?
How does Spring discover Beans?
When are Beans created?
How does Spring determine
which dependency to inject?
Those questions lead directly into the next lesson.
Engineering Principle
The core principle is:
A component should declare the collaborators it needs instead of constructing or globally discovering them itself.
Another:
Handler receives the request, UseCase coordinates the operation, Repository owns persistence, and Service represents an external capability.
And:
Dependency Injection is a composition mechanism. Good architecture still determines which dependencies are appropriate.
Summary
In this lesson, we established that:
- A dependency is a collaborator required by a component.
- Dependency Injection means receiving collaborators rather than constructing them internally.
- DI is a general design technique and is not unique to Spring.
- Plain Java can perform Dependency Injection manually.
- Spring automates application composition as the dependency graph grows.
- Constructor Injection is our default for required dependencies.
- Constructor Injection makes dependencies explicit.
- Required collaborators can remain
final. - Constructor Injection makes components easy to construct in tests.
- Field Injection hides construction requirements and is not our preferred style.
- Setter Injection is better suited to genuinely optional/changeable dependencies.
- Modern Spring normally does not require explicit
@Autowiredon a single constructor. - Our internal application flow remains
Handler → UseCase → Repository. - A Handler owns incoming transport/application-boundary concerns.
- A UseCase coordinates one meaningful application workflow.
- A Repository provides persistence capabilities.
Serviceremains reserved for external or third-party capabilities.- We do not create generic internal
OrderService,ProductService, orInventoryServiceclasses. SetInventoryUseCaserepresents the exact-quantity Inventory administration workflow.- DI does not require interfaces for every application class.
- Interfaces are useful when they protect meaningful boundaries.
PaymentServiceis a meaningful interface because it isolates an external system.- Domain objects such as
Order,OrderItem,Product, andInventoryare not shared injected application components. - Creating Domain objects with
newor factory methods is normal. - Stable collaborators belong in constructors.
- Request-specific values such as
CustomerId,OrderId, and commands belong in method parameters. - Spring-managed UseCases should generally remain stateless across requests.
- UseCases should not construct Repositories or third-party clients directly.
- Business code should not use
ApplicationContextas a service locator. - Static/global dependency access should be avoided.
- Constructor dependency lists make architecture visible.
- A large constructor can reveal an overly broad responsibility.
- Circular UseCase dependencies usually indicate unclear workflow ownership.
- Cross-capability workflows should have one clear UseCase owner.
- DI does not eliminate coupling; it makes coupling explicit and externally supplied.
- DI does not solve transaction management, concurrency, security, or external failure semantics.
- Domain behaviour remains in Domain objects even when application components use DI.
- Spring's role is to create and connect the application's long-lived components.
Next lesson:
Beans and the Application Context
There, we will examine what Spring actually creates and manages, what a Bean is, what the Application Context does, how component scanning finds application components, and how Spring builds the Handler → UseCase → Repository dependency graph during startup.
