Building the Spring Boot Application

Dependency Injection

LuettavaEsikatselu

Katselet ilmaista esikatseluoppituntia.

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:

  1. creates the Repository
  2. creates the UseCase
  3. 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 @Autowired on 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.
  • Service remains reserved for external or third-party capabilities.
  • We do not create generic internal OrderService, ProductService, or InventoryService classes.
  • SetInventoryUseCase represents the exact-quantity Inventory administration workflow.
  • DI does not require interfaces for every application class.
  • Interfaces are useful when they protect meaningful boundaries.
  • PaymentService is a meaningful interface because it isolates an external system.
  • Domain objects such as Order, OrderItem, Product, and Inventory are not shared injected application components.
  • Creating Domain objects with new or 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 ApplicationContext as 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.