Implementing Business Workflows

UseCase Responsibilities

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

এখন পর্যন্ত আমরা application-এর বেশিরভাগ foundation তৈরি করেছি:

HTTP API
Domain Model
PostgreSQL Schema
JPA Mapping
Repositories
Flyway Migrations
Pagination
Indexes

কিন্তু এগুলো individually useful হলেও এখনও একটি complete business operation তৈরি করে না।

একজন Customer যখন:

POST /api/v1/orders

call করে, তখন শুধু:

Controller request parse করবে

অথবা:

Repository Order save করবে

এতটুকু যথেষ্ট নয়।

একটি complete Order creation workflow-তে আমাদের করতে হবে:

Customer identify করা

requested Products load করা

Products orderable কিনা check করা

duplicate Product lines reject করা

Inventory check করা

server-side price নেওয়া

Order তৈরি করা

Inventory decrease করা

Order persist করা

এবং পুরো local database change-টি:

all-or-nothing

হতে হবে।

এই orchestration-এর দায়িত্ব আমাদের:

UseCase

-এর।

আমাদের architecture:

Handler
    ↓
UseCase
    ↓
Repository

এবং যেখানে external system আছে:

Handler
    ↓
UseCase
    ├── Repository
    └── External Service

এই lesson-এর goal:

একটি UseCase-এর exact responsibility বোঝা—কী orchestration সেখানে থাকবে, কী Domain-এর মধ্যে থাকবে, কী Repository-এর মধ্যে থাকবে, transaction boundary কোথায় থাকবে, এবং কেন আমরা একটি giant internal OrderService বানাব না।


What Is a UseCase?

একটি UseCase represents:

One meaningful application operation.

Examples:

CreateProductUseCase

DeactivateProductUseCase

SetInventoryUseCase

CreateOrderUseCase

GetOrderUseCase

ListCustomerOrdersUseCase

CancelOrderUseCase

Later:

PayOrderUseCase

Each UseCase answers:

The application is being asked to do what?

For example:

CreateOrderUseCase

does not represent:

all Order-related logic

It represents exactly:

Create an Order

UseCase Is an Application Workflow

A UseCase coordinates several pieces of the system.

For Create Order:

CreateOrderUseCase
    ↓
ProductRepository
    ↓
InventoryRepository
    ↓
Order domain
    ↓
OrderRepository

The UseCase understands the sequence of the operation.

For example:

1. identify Customer

2. validate request-level/application-level conditions

3. load Products

4. ensure Products are orderable

5. load/check Inventory

6. capture current server-side prices

7. create Order

8. decrease Inventory

9. persist changes

No single Domain Entity or Repository should own this whole sequence.


Why Not Put Everything in Order?

Suppose we try:

order.create(
        productRepository,
        inventoryRepository,
        customerId
);

Now the domain entity knows:

Repositories

persistence

database access

That is wrong direction.

Domain should understand:

valid Order state

valid Order transitions

OrderItem invariants

total calculation

It should not know:

where Product comes from

how Inventory is stored

which Customer is authenticated

how transaction is managed

Those are application workflow concerns.


Why Not Put Everything in Repository?

Another bad direction:

orderRepository.createOrder(
        customerId,
        requestItems
);

and inside Repository:

load Products

check Product active

calculate price

decrease Inventory

create Order

decide business errors

Now persistence layer has become the business application.

Repository should answer persistence questions such as:

find this Product

load this Inventory

persist this Order

perform this atomic Inventory update

It should not invent the business workflow around them.


Why Not Put Everything in Handler?

Bad:

@PostMapping
public ResponseEntity<?> createOrder(...) {

    Product product =
            productRepository.findById(...);

    Inventory inventory =
            inventoryRepository.findById(...);

    if (!product.isActive()) {
        ...
    }

    inventory.decrease(...);

    Order order = ...;

    orderRepository.save(order);

    ...
}

Now HTTP transport code owns:

business orchestration

persistence

transactions

domain decisions

The Handler becomes difficult to test and impossible to reuse outside HTTP.

Our Handler should remain:

HTTP boundary

not:

application workflow

Responsibility Split

A useful mental model:

Handler
→ Translate transport
UseCase
→ Coordinate operation
Domain
→ Protect business state
Repository
→ Persist/query data
External Service
→ Communicate with external system

This distinction is the foundation of our application architecture.


Handler Responsibilities

A Handler may:

read path/query/body

trigger request validation

obtain authenticated identity from security boundary

convert Request DTO → UseCase input

invoke UseCase

convert application result → Response DTO

return appropriate success status

For example:

POST /api/v1/orders

Handler receives:

{
  "items": [
    {
      "productId": "101",
      "quantity": 2
    }
  ]
}

It does not calculate:

unitPrice

total

inventory effect

OrderStatus

Those values are server-controlled.


UseCase Responsibilities

A UseCase commonly owns:

operation sequencing

cross-domain coordination

repository coordination

ownership/permission checks relevant to the operation

business decisions requiring multiple pieces of state

transaction boundary for local database work

calling domain behaviour

translating persistence/external outcomes
into application outcomes

It should read like the business operation.


Domain Responsibilities

Domain objects own rules about their own valid state.

Examples:

Product
→ price cannot be negative
→ deactivate itself
Inventory
→ quantity cannot become negative
OrderItem
→ quantity must be positive
→ unitPrice cannot be negative
→ subtotal = quantity × unitPrice
Order
→ starts UNPAID
→ PAID cannot be cancelled
→ CANCELLED cannot be paid
→ total derives from items

These rules should remain true regardless of whether the caller is:

REST API

scheduled job

test

future internal workflow

Repository Responsibilities

Repository handles:

load persisted state

save persisted state

execute efficient queries

apply database pagination

perform persistence-specific atomic operations

translate persistence representation

It may know:

JPA

Hibernate

SQL

PostgreSQL

locking/query details

UseCase should not.


Cross-Entity Rules Belong in UseCase

Consider:

Can this Customer order 3 units of Product 101?

Answer depends on multiple pieces of state:

Product exists?

Product active?

Inventory exists?

Inventory >= 3?

Customer authenticated?

No single Entity owns all of that.

This is a strong sign that the rule belongs to:

CreateOrderUseCase

or a persistence operation coordinated by it.


Local Invariant vs Workflow Rule

Compare:

OrderItem.quantity > 0

This is intrinsic to OrderItem.

So:

Domain

owns it.

But:

requested quantity <= available Inventory

requires:

requested Order line
+
current Inventory

This is cross-object/current-state logic.

So:

UseCase

coordinates it.


Example: Create Order

Our accepted workflow:

Create Order
    ↓
identify Customer
    ↓
load Products
    ↓
check Product orderability
    ↓
check Inventory
    ↓
capture current Product price
    ↓
build Order + OrderItems
    ↓
decrease Inventory
    ↓
persist Order

Let's examine where each step belongs.


1. Identify Customer

The HTTP client does not send:

{
  "customerId": "123"
}

and choose Order ownership.

Authenticated identity is server-controlled.

Conceptually:

Security boundary
    ↓
CustomerId
    ↓
CreateOrderUseCase

The UseCase receives or obtains the authenticated Customer identity through the approved application/security boundary.

Order ownership is then derived from:

authenticated CustomerId

not request body.


2. Validate Duplicate Product Lines

Request could contain:

Product 101 × 2

Product 101 × 3

Our contract says duplicate Product lines are rejected.

The Handler's Bean Validation cannot easily determine this as a normal field constraint.

CreateOrderUseCase can detect duplicate ProductIds before persistence.

Domain Order can also protect the invariant so invalid state cannot be constructed accidentally elsewhere.

That gives us:

UseCase
→ early application rejection

Domain
→ final business invariant

3. Load Products

UseCase calls:

ProductRepository

for requested ProductIds.

It needs the authoritative current Product state.

Not:

client-provided name

client-provided price

The server determines business truth.


4. Check Product Orderability

A Product must be:

existing

active

for a new Order.

This may involve:

Product domain state

plus application decision:

can this Product participate in this operation?

UseCase coordinates the check.

If Product was deactivated:

CreateOrderUseCase
→ reject

with an application failure such as:

PRODUCT_NOT_ORDERABLE

Later HTTP boundary maps that to:

409

5. Load and Check Inventory

UseCase coordinates:

ProductId
→ InventoryRepository
→ Inventory state

Then ensures requested quantity can be consumed.

But there is an important production concern:

concurrency

A simple:

read quantity
→ check
→ write quantity

may not be enough when two requests race.

So UseCase owns the business intent:

consume this quantity if available

while Repository may eventually implement the concurrency-safe database mechanics.


6. Capture Server-Side Price

Client may have seen:

100.00

during Product browsing.

But browsing price is not a guaranteed quote.

At successful Order creation:

CreateOrderUseCase

loads the current Product state and captures:

Product.price

into:

OrderItem.unitPrice

This preserves purchase-time price.


7. Create the Domain Order

After required current data has been gathered, UseCase creates:

Order

OrderItems

Domain protects local invariants such as:

at least one item

positive quantity

valid price

no duplicate Product line

initial status UNPAID

The UseCase should not manually manipulate:

order.status = UNPAID

through arbitrary setters.

Use the domain construction behaviour.


8. Decrease Inventory

Our accepted v1 semantics:

Order creation
→ consumes available Inventory

No reservation table.

No expiry.

No temporary hold.

The UseCase coordinates Inventory mutation as part of Order creation.


9. Persist Order

Finally:

OrderRepository

persists:

Order

OrderItems

and returns the generated Order identity where required.

All local state changes must succeed together.

That brings us to one of the most important UseCase responsibilities:

transaction boundary

UseCase as Transaction Boundary

Create Order modifies multiple pieces of persistent state:

Inventory

Order

OrderItems

Suppose:

Inventory decreases

but then:

Order insert fails

If Inventory committed separately, system becomes inconsistent:

stock consumed

no Order exists

We need:

all succeed

or:

all rollback

Transaction Around the Operation

Conceptually:

@Transactional
public CreateOrderResult execute(...) {

    // load/check Products

    // consume Inventory

    // create Order

    // persist Order

    return result;
}

The important point is not the annotation itself.

The important point is:

One business operation defines one atomic local database boundary.


Why Transaction Usually Belongs on UseCase

If transaction exists only inside individual Repository calls:

InventoryRepository.save()
→ commit

then:

OrderRepository.save()
→ separate commit

Create Order is no longer atomic.

The UseCase is the layer that knows:

these changes together form one operation

Therefore it is the natural transaction owner.


Repository Participates in the Transaction

Inside:

CreateOrderUseCase transaction

we may call:

ProductRepository

InventoryRepository

OrderRepository

Their JPA operations participate in the same transaction.

Architecture:

CreateOrderUseCase
    │
    │ transaction
    │
    ├── ProductRepository
    ├── InventoryRepository
    └── OrderRepository

The transaction follows the workflow.


Transaction Does Not Belong in Domain

Avoid:

@Transactional
public void cancel() {
    ...
}

inside:

Order

Domain should not know:

Spring transaction management

database

order.cancel() only protects lifecycle logic.

CancelOrderUseCase decides that cancellation plus Inventory restore must happen atomically.


Transaction Does Not Belong in Handler

Avoid:

@PostMapping(...)
@Transactional
public ResponseEntity<?> cancelOrder(...) {
    ...
}

Now HTTP transport owns application transaction semantics.

If the same UseCase is later invoked from another adapter, transaction behaviour may disappear.

Put the boundary with the operation.


Example: Cancel Order

Accepted cancellation flow:

Customer requests cancellation
    ↓
load Order
    ↓
verify ownership
    ↓
Order.cancel()
    ↓
restore Inventory
    ↓
persist changes

Important rules:

only unpaid Order can be cancelled

already paid → reject

already cancelled → reject

Customer must own Order

Inventory must be restored

Which Layer Owns Which Cancellation Rule?

Ownership

Requires:

authenticated CustomerId

Order.customerId

UseCase coordinates this check.


Lifecycle

Rule:

UNPAID → CANCELLED

allowed.

But:

PAID → CANCELLED

not allowed in v1.

This belongs in:

order.cancel();

Domain owns its lifecycle.


Inventory Restoration

Cancellation affects:

Order
+
Inventory

So UseCase coordinates restoration.

Order itself should not call:

InventoryRepository

or modify external Inventory objects.


Transaction

These must be atomic:

Order status → CANCELLED

Inventory restored

Otherwise we could get:

Order cancelled

Inventory not restored

or:

Inventory restored

Order still UNPAID

So:

CancelOrderUseCase

owns the local transaction.


A UseCase Should Read Like the Operation

A well-designed UseCase often reads approximately like:

public CancelOrderResult execute(
        CancelOrderCommand command
) {
    Order order =
            loadOrder(command.orderId());

    ensureOwnedBy(
            order,
            command.customerId()
    );

    order.cancel();

    restoreInventory(
            order.items()
    );

    orderRepository.save(order);

    return ...;
}

Notice what is missing:

HTTP status codes

JSON

JPA entities

EntityManager

SQL

Payment provider HTTP calls

The code communicates:

business operation

UseCase Input

A UseCase should not depend directly on HTTP Request DTO if doing so couples application flow to transport.

For example, HTTP model:

public record CreateOrderRequest(
        List<CreateOrderItemRequest> items
) {
}

Application input may be:

public record CreateOrderCommand(
        CustomerId customerId,
        List<CreateOrderItemCommand> items
) {
}

The distinction is useful when application semantics differ from transport.


Don't Create Commands Just for Ceremony

If an operation has:

one ProductId

a separate:

DeactivateProductCommand

may not add value.

You can simply have:

execute(ProductId productId);

Use a Command/input object when it makes the UseCase API clearer.

Do not build a command framework around every method.


UseCase Output

Similarly, UseCase may return:

Product

Order

OrderSummary

operation-specific result

depending on what the Handler needs.

Avoid returning:

ResponseEntity

ApiProblem

HttpStatus

because those belong to HTTP.


Application Errors

UseCase may produce application-level failures:

PRODUCT_NOT_FOUND

PRODUCT_NOT_ORDERABLE

INSUFFICIENT_INVENTORY

ORDER_NOT_FOUND

ORDER_NOT_CANCELLABLE

ORDER_NOT_PAYABLE

These describe:

application meaning

not HTTP formatting.

Later error boundary maps them to:

404

409

...

and our canonical Problem response.


Business Error vs System Error

Suppose Inventory is insufficient.

This is expected:

business conflict

The UseCase should expose a meaningful application error.

But PostgreSQL connection failure is:

system/infrastructure failure

Do not convert it into:

INSUFFICIENT_INVENTORY

just because it happened during Inventory access.

Failure meaning must remain accurate.


UseCase Should Not Catch Everything

Bad:

try {
    ...
} catch (Exception exception) {
    throw new OrderCreationFailedException();
}

Now:

Product missing

Inventory insufficient

DB outage

mapping bug

can all become the same error.

This destroys useful failure semantics.

Catch/translate only when a specific boundary requires it.


One UseCase per Meaningful Operation

Avoid a giant:

OrderService

with:

createOrder()

getOrder()

listOrders()

cancelOrder()

payOrder()

refundOrder()

validateOrder()

calculateTotal()

...

This class slowly becomes:

everything related to Orders

and accumulates:

too many dependencies

too many transaction modes

too many reasons to change

Prefer Operation-Oriented UseCases

Instead:

CreateOrderUseCase

GetOrderUseCase

ListCustomerOrdersUseCase

CancelOrderUseCase

PayOrderUseCase

Each operation has:

focused dependencies

focused tests

clear transaction needs

clear input/output

This is simpler to reason about.


Avoid Fragmenting Into Tiny UseCases

The opposite extreme is also bad.

Suppose CreateOrder becomes:

ValidateCustomerUseCase

LoadProductUseCase

ValidateProductUseCase

LoadInventoryUseCase

CheckInventoryUseCase

CalculateOrderTotalUseCase

SaveOrderUseCase

Then CreateOrderUseCase just calls seven other UseCases.

We have created ceremony rather than clarity.


A UseCase Is Not a Function for Every Step

UseCase should represent:

user/application operation

not:

every internal line of business logic

Internal private methods or Domain methods are enough for sub-steps.


Avoid UseCase-to-UseCase Chains by Default

For example:

CreateOrderUseCase
→ GetProductUseCase

is usually unnecessary.

Both are application operations.

CreateOrderUseCase should directly depend on:

ProductRepository

for the Product state it needs.

Otherwise internal workflows become unnecessarily coupled to other endpoint-oriented operations.


Shared Domain Behaviour Is Different

If both:

CreateOrderUseCase

UpdateProductUseCase

need Product's rule:

price cannot be negative

that belongs in:

Product

not a shared:

ValidateProductUseCase

Domain is the place for reusable intrinsic business behaviour.


Repositories Are Reusable Boundaries

Multiple UseCases may use:

OrderRepository

because persistence capability is reusable.

That's expected.

Example:

GetOrderUseCase
    ↓
OrderRepository
CancelOrderUseCase
    ↓
OrderRepository
PayOrderUseCase
    ↓
OrderRepository

Each operation remains independent while sharing the persistence abstraction.


Keep UseCases Stateless

Spring application components are typically singleton Beans.

A UseCase should therefore not keep request-specific state in fields.

Bad:

@Component
public class CreateOrderUseCase {

    private CustomerId currentCustomer;

    private List<Product> currentProducts;

    ...
}

Concurrent requests could interfere with each other.

Use method-local state:

public Result execute(
        Command command
) {
    CustomerId customerId =
            command.customerId();

    ...
}

Dependencies can be fields.

Request data should not.


Constructor Injection

UseCase dependencies are explicit:

@Component
public class CancelOrderUseCase {

    private final OrderRepository orderRepository;
    private final InventoryRepository inventoryRepository;

    public CancelOrderUseCase(
            OrderRepository orderRepository,
            InventoryRepository inventoryRepository
    ) {
        this.orderRepository =
                orderRepository;

        this.inventoryRepository =
                inventoryRepository;
    }
}

This tells us immediately:

what infrastructure this operation needs

and makes plain unit testing straightforward.


Do Not Inject Everything "Just in Case"

Bad:

CreateOrderUseCase(
    ProductRepository,
    InventoryRepository,
    OrderRepository,
    PaymentService,
    Clock,
    ObjectMapper,
    EntityManager,
    ApplicationContext,
    LoggerFactory,
    ...
)

Dependencies should correspond to real responsibilities.

For v1 Create Order:

PaymentService

does not belong there because payment is a separate explicit operation:

POST /orders/{id}/pay

UseCase and Current Time

If an operation genuinely needs:

current time

such as Order.createdAt, a deliberate time source can make testing deterministic.

For example:

Clock

can be useful when time becomes application behaviour.

But do not introduce a global abstraction for time unless the operation needs controlled time semantics.

Use practical judgement.


UseCase Should Not Read Environment Variables

Bad:

String url =
        System.getenv(
                "PAYMENT_URL"
        );

inside UseCase.

Configuration belongs in infrastructure/configuration.

External Services receive their configuration separately.

UseCase sees:

PaymentService

not:

provider URL

API key

HTTP timeout

UseCase Should Not Know JPA

Avoid:

entityManager.find(...);

entityManager.lock(...);

repository.flush();

inside UseCase.

If inventory correctness later requires:

conditional SQL update

row lock

Repository exposes an application-meaningful operation and persistence adapter implements the JPA/PostgreSQL mechanics.


Example: Concurrency-Safe Inventory Intent

Instead of UseCase doing:

entityManager
        .createNativeQuery(...)

we might eventually have Repository semantics conceptually like:

boolean consumeIfAvailable(
        ProductId productId,
        int quantity
);

Then UseCase says:

consume quantity if available

Repository decides:

how PostgreSQL safely performs it

This preserves the boundary.


UseCase May Coordinate Multiple Repositories

That is not a design smell by itself.

CreateOrderUseCase naturally needs:

ProductRepository

InventoryRepository

OrderRepository

because the operation crosses three application capabilities.

Trying to hide that by creating:

OrderCreationRepository

that does everything may reduce constructor parameters but destroy responsibility clarity.


Cross-Capability Coordination Is the UseCase's Job

Our modular monolith has capability boundaries:

Product

Inventory

Order

but they run:

inside one application

A workflow can coordinate them in-process.

We do not need:

Product HTTP API call

Inventory HTTP API call

Kafka event

inside the application.

Those would introduce distributed-system complexity without a requirement.


UseCase Is Not a Microservice

A class named:

CreateOrderUseCase

is not a deployable service.

It is simply:

application logic inside our modular monolith

All modules still run in:

one JVM

one deployable application

one PostgreSQL database

No Internal Service Layer

You may see traditional Spring tutorials with:

Controller
→ Service
→ Repository

For this project we use:

Handler
→ UseCase
→ Repository

because:

UseCase

communicates application operation more precisely.

We reserve:

Service

for external systems/integrations such as:

PaymentService

This keeps terminology meaningful.


Why Service Is Reserved for External Integrations

Later:

PayOrderUseCase

may need:

PaymentService

The distinction becomes visually useful:

PayOrderUseCase
    ├── OrderRepository
    └── PaymentService

We can immediately understand:

Repository
→ local persistence
Service
→ external capability

No ambiguity between:

OrderService

PaymentService

ProductService

where some are internal workflows and others are remote integrations.


External Calls Change Transaction Thinking

Suppose future:

PayOrderUseCase

does:

load Order

call Payment Provider

mark Order PAID

We cannot make:

PostgreSQL transaction
+
remote Payment Provider request

one atomic database transaction.

If provider succeeds and our DB write fails:

external payment succeeded

local Order may still be UNPAID

This requires:

idempotency

provider references

failure recovery

not a giant @Transactional annotation.

We'll design that in Module 9.


Avoid Long DB Transactions Around External Calls

Bad conceptual pattern:

BEGIN DB TRANSACTION

lock Order

call remote Payment Provider

wait 5 seconds

update Order

COMMIT

This may hold database resources while waiting on network I/O.

A UseCase owns workflow semantics, but transaction design must respect:

local vs external boundary

Local DB atomicity and external integration reliability are different problems.


UseCase and Authorization

Not every authorization rule belongs in the security filter layer.

Endpoint-level role access can be handled through Spring Security.

But operation-specific ownership often belongs close to the UseCase.

Example:

Customer can cancel only own Order

UseCase needs:

authenticated CustomerId

target Order

to enforce that rule.


Role vs Ownership

Role:

CUSTOMER

ADMIN

may determine:

is this endpoint/action allowed at all?

Ownership determines:

does this Customer own this specific Order?

These are different checks.

Later Security module will define how they fit together.


Do Not Put Roles Inside Order

Bad:

order.cancel(
        currentUserRole
);

Order does not care whether the caller is:

CUSTOMER

ADMIN

unless role itself changes intrinsic Order lifecycle semantics.

Domain only knows:

Can an Order in this state be cancelled?

UseCase/security layer knows:

Is this caller allowed to attempt it?

UseCase and Problem Details

UseCase should not build:

{
  "type": "...",
  "status": 409,
  "instance": "..."
}

That is transport representation.

UseCase produces application meaning:

InsufficientInventory

OrderNotCancellable

ProductNotFound

HTTP error handler turns that into our canonical:

type

title

status

detail

instance

code

errors

shape.


UseCase Testing

UseCases are excellent unit-test targets because dependencies are explicit.

For:

CancelOrderUseCase

we can use Repository doubles to test:

own UNPAID Order
→ cancellation succeeds

paid Order
→ reject

cancelled Order
→ reject

another Customer's Order
→ inaccessible/reject

successful cancellation
→ Inventory restored

failure
→ expected application error

No HTTP server required.

No PostgreSQL required when testing application orchestration.


What UseCase Unit Tests Should Not Test

Avoid verifying implementation trivia:

repository.findById called exactly once

unless interaction count is important to correctness.

Prefer observable behaviour:

correct result

correct domain transition

correct persistence intent

correct failure

Tests should survive harmless refactoring.


Repository Integration Tests Cover Persistence

Separate tests prove:

OrderRepository actually persists Order

Inventory concurrency operation works

JPA mappings are correct

PostgreSQL constraints work

UseCase tests don't need to rediscover those mechanics.

This keeps test responsibilities clean.


Handler Tests Cover Transport

Handler/API tests verify:

request validation

HTTP status

Problem response

DTO mapping

authentication integration

Again, one layer doesn't need to prove everything.


UseCase Method Naming

A focused UseCase can simply expose:

execute(...)

because the class name already communicates the operation:

createOrderUseCase.execute(...);

cancelOrderUseCase.execute(...);

No need for:

createOrderUseCase.createOrder(...);

unless team conventions prefer explicit verb names.

Keep naming consistent and unsurprising.


UseCase Size

How large should a UseCase be?

There is no meaningful rule such as:

maximum 50 lines

A UseCase should be:

large enough to express one operation clearly

small enough that unrelated operations
do not accumulate inside it

If Create Order needs several clear private methods:

validateDuplicateProducts()

loadProducts()

consumeInventory()

buildOrder()

that's fine.

Do not split purely to satisfy line-count aesthetics.


Private Methods vs New Components

Suppose:

private void ensureOrderable(
        Product product
) {
    ...
}

is used only inside CreateOrderUseCase.

It can remain private.

Don't immediately create:

ProductOrderabilityValidator

ProductEligibilityManager

OrderingPolicyService

unless there is actual reusable domain/application behaviour that deserves a named abstraction.


Avoid Manager, Helper, Processor

Vague classes often emerge when responsibilities are unclear:

OrderManager

OrderHelper

OrderProcessor

OrderUtils

Ask instead:

Is this a UseCase?

Is this domain behaviour?

Is this persistence?

Is this external integration?

Clear boundaries usually remove the need for vague names.


UseCase Composition With Domain

Good architecture often reads like:

UseCase
→ asks Repository for state
→ tells Domain to perform behaviour
→ persists resulting state

Example:

CancelOrderUseCase

does not ask:

if (
    order.status()
            == OrderStatus.UNPAID
) {
    order.setStatus(
            OrderStatus.CANCELLED
    );
}

Instead:

order.cancel();

The UseCase coordinates.

The Domain decides whether its own transition is valid.


"Tell, Don't Ask" Without Extremism

We avoid anemic domain code like:

UseCase reads every field

UseCase decides every intrinsic rule

UseCase directly mutates every state

But we also don't force every cross-resource rule into a Domain Entity.

Healthy split:

UseCase
→ coordinates

Domain
→ protects local truth

Example: Order.total()

UseCase does not need:

BigDecimal total =
        order.items()
                .stream()
                .map(...)
                .reduce(...);

if:

order.total();

already represents domain behaviour.

Likewise:

OrderItem.subtotal()

belongs with OrderItem.


Example: Inventory Availability

Domain Inventory can protect:

inventory.decrease(quantity);

from becoming negative in a simple in-memory context.

But concurrency-safe availability across multiple requests requires Repository/database support.

So both can participate:

Domain
→ local invariant
Repository/PostgreSQL
→ concurrent persistence correctness
UseCase
→ coordinates the operation

The UseCase Does Not Replace the Domain

If we put all rules into UseCase:

Order becomes data

Product becomes data

Inventory becomes data

and application layer eventually contains thousands of:

if status == ...

if price < ...

if quantity ...

This recreates the anemic domain problem we already discussed.


The Domain Does Not Replace the UseCase

Likewise if Entities try to:

load each other

call repositories

check authenticated user

start transactions

the domain becomes an application framework.

Both layers are needed because they solve different types of reasoning.


A Useful Responsibility Test

Ask:

Can this rule be decided using only this object's own valid state?

If yes, likely Domain.

Example:

Can this Order transition from PAID to CANCELLED?

Order knows.

If rule requires:

authenticated Customer

another aggregate

current database state

external provider

it likely needs UseCase coordination.


Another Responsibility Test

Ask:

Is this about how data is stored or fetched?

If yes:

Repository/persistence

Examples:

JPQL

SQL

locking

pagination

JPA mappings

Another Responsibility Test

Ask:

Is this about HTTP representation?

If yes:

Handler/transport

Examples:

@ResponseStatus

Request DTO

ApiProblem

Location header

Another Responsibility Test

Ask:

Is this an external capability?

If yes:

Service

Example:

PaymentService

UseCase coordinates that Service when the operation requires it.


Create Order Responsibility Map

Let's put the complete flow together.

Handler

POST /api/v1/orders

validate request shape

obtain authenticated Customer identity

map request → command

invoke CreateOrderUseCase

map result → response

return 201 + Location

CreateOrderUseCase

reject duplicate Product IDs

load Product state

ensure Products are orderable

coordinate Inventory availability/consumption

capture server-side current Product prices

construct Order + OrderItems

persist Order

own local transaction

Product

protect Product state

price invariant

active state

Inventory

protect quantity invariant

represent available quantity

Order

require valid items

start UNPAID

derive total

protect lifecycle

Repositories

load Products

load/update Inventory safely

persist Order

implement PostgreSQL mechanics

PostgreSQL

durability

constraints

transaction

concurrency primitives

This is the architecture working as a system.


Cancel Order Responsibility Map

Handler

read orderId

obtain authenticated Customer

invoke UseCase

map success/error

CancelOrderUseCase

load Order

ensure Customer ownership

invoke Order.cancel()

restore Inventory

persist state

own local transaction

Order

UNPAID → CANCELLED

reject PAID cancellation

reject repeated cancellation

Inventory

increase available quantity

Repositories

load Order

persist Order

persist Inventory

Pay Order Preview

Later:

PayOrderUseCase

will likely coordinate:

load Order

ownership

payability

PaymentService

payment outcome

Order.markPaid()

Notice:

PaymentService

appears because Payment is external.

We do not rename:

PayOrderUseCase

to:

OrderService

just because it coordinates several things.


Common Mistake 1 — Giant OrderService

One class becomes responsible for every Order operation and accumulates unrelated dependencies.

Prefer operation-focused UseCases.


Common Mistake 2 — One UseCase per Tiny Internal Step

This creates orchestration ceremony and UseCase-to-UseCase chains.

A UseCase represents a meaningful application operation.


Common Mistake 3 — Business Logic in Handler

HTTP transport should not own application workflows.


Common Mistake 4 — Repository Becomes the Business Layer

Persistence implementations should not decide Order lifecycle, ownership, or pricing policy.


Common Mistake 5 — Domain Calls Repositories

Domain objects should remain framework/persistence independent.


Common Mistake 6 — UseCase Uses JPA Entities

Application workflow should speak in domain/application models, not persistence representations.


Common Mistake 7 — UseCase Uses EntityManager

Database mechanics belong behind Repository.


Common Mistake 8 — Transaction Per Repository Call

Multi-repository business operations may lose atomicity.

The UseCase owns the overall local transaction where required.


Common Mistake 9 — Long Database Transaction Around Remote API

External systems cannot be made atomic with local PostgreSQL by holding a transaction open.


Common Mistake 10 — UseCase Builds HTTP Error Responses

Application failure meaning and transport representation should remain separate.


Common Mistake 11 — UseCase Keeps Request State in Fields

Singleton application components should remain stateless.


Common Mistake 12 — Client Supplies Server-Controlled State

UseCase derives:

CustomerId

price

total

status

from trusted application state.


Common Mistake 13 — Service Used for Every Internal Class

Our terminology stays:

Handler
→ transport

UseCase
→ internal application workflow

Repository
→ persistence

Service
→ external integration

Common Mistake 14 — UseCase-to-UseCase Dependency by Default

Operations should generally share Domain/Repository abstractions rather than call one another.


Common Mistake 15 — Cross-Aggregate Rule Forced Into One Entity

Rules involving Product + Inventory + Customer belong in workflow coordination.


UseCase Review Checklist

For every UseCase, ask:

What single application operation does this represent?

What input does it actually need?

Which state must be loaded?

Which Repositories are required?

Which rules belong to Domain instead?

Does the operation cross multiple aggregates?

Does it need one local transaction?

Is ownership checked at the correct layer?

Is server-controlled data derived rather than trusted?

Is any JPA/SQL leaking into the UseCase?

Is any HTTP representation leaking into the UseCase?

Is any external system represented through a Service?

Is the UseCase stateless?

Could unrelated operations be split into their own UseCases?

Have we created unnecessary helper/manager abstractions?

Our Architecture Going Forward

From this point, business workflow implementation will follow:

Handler
    ↓
UseCase
    ↓
Domain
    ↓
Repository
    ↓
PostgreSQL

Where appropriate, more accurately:

Handler
    ↓
UseCase
    ├── Domain
    ├── Repository
    └── External Service

The UseCase sits at the center because it understands:

what operation is being performed

without owning:

HTTP

SQL

database mapping

external protocol details

Engineering Principle

The core principle:

A UseCase coordinates one meaningful application operation. It orchestrates state and dependencies, while Domain objects protect their own invariants and Repositories own persistence mechanics.

Another:

Transaction boundaries should follow business operations, not individual Repository methods. If several local database changes form one operation, the UseCase is the natural place to define their atomic boundary.

And:

UseCases should make workflows easier to read—not create another layer of generic Service, Manager, and Helper abstractions.


Summary

In this lesson, we learned that:

  • A UseCase represents one meaningful application operation.
  • Our architecture remains Handler -> UseCase -> Repository.
  • Domain behaviour participates inside the workflow rather than being replaced by the UseCase.
  • Service remains reserved for external integrations such as the future PaymentService.
  • Handlers own HTTP/transport concerns.
  • UseCases own application orchestration.
  • Domain objects protect local business invariants and lifecycle rules.
  • Repositories own persistence queries and database mechanics.
  • Cross-entity and cross-capability rules naturally require UseCase coordination.
  • Create Order coordinates Customer identity, Products, Inventory, pricing, Order construction, and persistence.
  • Clients never control Customer ownership, purchase-time price, Order total, or lifecycle state.
  • Duplicate Product lines can be rejected early in the UseCase while the Domain also protects the invariant.
  • Product orderability depends on current server-side Product state.
  • Purchase-time unitPrice is captured from current Product state during successful Order creation.
  • Inventory is consumed during Order creation in our v1 model.
  • Create Order requires one local transaction covering Inventory mutation and Order persistence.
  • Cancel Order coordinates ownership, Order.cancel(), Inventory restoration, and persistence.
  • Order itself owns whether its lifecycle transition is valid.
  • UseCase owns the transaction because it knows which local changes form one atomic operation.
  • Transactions should not live in Domain objects or HTTP Handlers.
  • Repository-level transactions alone are insufficient when one business workflow spans several repositories.
  • Operation-specific UseCases are preferable to one giant internal OrderService.
  • We also avoid fragmenting one workflow into many tiny UseCases.
  • UseCases generally should not call other UseCases merely to reuse internal steps.
  • Shared intrinsic rules belong in Domain objects.
  • UseCases should remain stateless singleton components with explicit constructor-injected dependencies.
  • UseCases should not read environment variables, manipulate EntityManager, or know JPA entities.
  • Database-specific concurrency mechanisms remain behind Repository boundaries.
  • A UseCase can legitimately coordinate several capability repositories inside the same modular monolith.
  • Package/module boundaries do not require internal HTTP calls or messaging.
  • UseCase errors describe application meaning rather than HTTP Problem Details.
  • Ownership checks requiring authenticated identity and target resource state naturally belong in the application workflow.
  • External Service calls require different transaction thinking because PostgreSQL cannot make remote systems atomic.
  • UseCase unit tests can focus on orchestration using Repository doubles, while Repository integration tests verify actual PostgreSQL behaviour.

Next lesson:

Implementing Order Creation

There we will implement our first major end-to-end business workflow: Create Order—from authenticated Customer and request items through Product loading, duplicate detection, Inventory validation, purchase-time pricing, Order construction, Inventory consumption, persistence, and the first concrete transaction boundary.