Implementing Business Workflows
UseCase Responsibilities
আপনি একটি 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, andHelperabstractions.
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.
Serviceremains reserved for external integrations such as the futurePaymentService.- 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
unitPriceis 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.