Domain Modeling
Separating Domain Logic from Transport Logic
আপনি একটি free preview lesson দেখছেন।
একটি backend application-এ একই business operation বিভিন্ন layer-এর মধ্য দিয়ে যায়।
For example:
HTTP Request
↓
Handler
↓
UseCase
↓
Domain
↓
Repository
একজন customer যখন Order create করতে চায়, তখন incoming JSON থেকে শুরু করে persisted Order পর্যন্ত অনেক transformation ঘটে।
Problem শুরু হয় যখন আমরা সব logic এক জায়গায় লিখে ফেলি।
For example:
@PostMapping("/orders")
public OrderResponse createOrder(
@RequestBody CreateOrderRequest request
) {
// validate JSON
// find products
// check inventory
// calculate price
// create order
// save order
// return response
}
এটি প্রথমে simple মনে হতে পারে।
কিন্তু তখন HTTP Handler একসঙ্গে হয়ে যায়:
transport parser
validator
business workflow
domain logic
persistence coordinator
এই lesson-এর goal:
Transport concerns, application workflow, এবং domain behaviour-এর boundary পরিষ্কার রাখা।
What Is Transport Logic?
Transport logic হলো external communication protocol-এর responsibility।
আমাদের REST API-তে transport হলো:
HTTP + JSON
Transport layer deals with things such as:
HTTP method
URL/path parameters
query parameters
request body
JSON serialization/deserialization
HTTP status codes
request/response DTOs
basic request validation
These are not domain concepts।
What Is Domain Logic?
Domain logic represents business rules and state behaviour।
Examples:
Product price cannot be negative
Inventory quantity cannot go below zero
PAID Order cannot be cancelled
Order must contain at least one item
Order total derives from Order Items
These rules should remain true regardless of whether the operation is triggered through:
HTTP
test code
future scheduled job
future internal workflow
Domain logic should not depend on HTTP existing।
What Does the UseCase Own?
UseCase sits between transport and domain/infrastructure।
It coordinates a complete application operation।
For Create Order:
CreateOrderUseCase
may coordinate:
customer identity
Product lookup
Inventory lookup
cross-entity validation
current price capture
Inventory decrease
Order construction
persistence
The UseCase understands the business workflow across multiple concepts।
It should not care whether input came from:
JSON
CLI
another Handler
The Three Responsibilities
A useful mental model:
Handler
→ Understands transport
UseCase
→ Understands application workflow
Domain
→ Understands business state and local rules
Repository:
Repository
→ Understands persistence
External Service:
Service
→ Understands external/third-party protocol
Keeping these responsibilities clear prevents one class from becoming the entire application।
Example Request
Suppose client sends:
{
"items": [
{
"productId": "P-100",
"quantity": 2
},
{
"productId": "P-200",
"quantity": 1
}
]
}
This is transport data।
A request DTO might be:
public record CreateOrderRequest(
List<CreateOrderItemRequest> items
) {
}
and:
public record CreateOrderItemRequest(
String productId,
int quantity
) {
}
These classes describe the API input।
They are not Order domain entities।
What Should Handler Validate?
Handler or transport validation should handle things that belong to the request contract।
Examples:
request body is present
items field is present
JSON shape is valid
quantity can be parsed
productId has expected transport representation
required fields are present
These are transport-level concerns।
Example Transport Validation
Later Spring validation may let us express something conceptually like:
public record CreateOrderItemRequest(
@NotBlank
String productId,
@Positive
int quantity
) {
}
This helps reject clearly malformed requests early।
But transport validation is not our only protection।
Domain Rule Should Not Depend on HTTP Validation
Suppose Handler checks:
quantity > 0
Should OrderItem then accept:
new OrderItem(
productId,
-10,
price
);
because HTTP already validates it?
No।
If positive quantity is a domain invariant, the domain should protect it too।
Why?
Because OrderItem may later be created from:
another UseCase
test
import
internal process
The domain should not assume all callers came through one HTTP Handler।
Validation at Different Boundaries Is Not Always Duplication
Consider:
quantity must be positive
Transport validation says:
This request is invalid input.
Domain validation says:
This business object cannot exist with invalid quantity.
Those are related but different guarantees।
So both can be legitimate।
But Don't Repeat Every Rule Everywhere
Suppose rule:
PAID Order cannot be cancelled.
We do not need to encode this separately in:
Handler
UseCase
Order
The strongest owner is Order because it owns status।
So:
order.cancel();
should enforce the rule।
Handler does not need to independently duplicate the state-transition logic।
A Rule Ownership Test
For each validation/rule ask:
Which layer has the information and responsibility required to decide this correctly?
Examples follow।
Rule: JSON Field Missing
items field is missing
Owner:
Handler / transport validation
Why?
Because this is about incoming request shape।
Domain should not know a JSON property called items exists।
Rule: Order Must Have at Least One Item
Order cannot be empty.
Owner:
Order domain
Because a valid Order inherently requires items।
Even if input comes from somewhere other than HTTP, the rule remains true।
Rule: Product Must Exist
To decide this we need:
ProductRepository
Order itself cannot know whether arbitrary Product ID exists in the system।
So this belongs to:
CreateOrderUseCase
which coordinates persistence and domain state।
Rule: Product Must Be Active
There are two responsibilities here।
Product can tell us its own state:
product.isActive();
or protect operations based on that state।
But the Create Order workflow must decide:
This Product may participate in this operation.
So:
CreateOrderUseCase
coordinates the decision using Product domain state।
Rule: Inventory Must Be Sufficient
The Inventory object owns:
availableQuantity
and can protect:
inventory.decrease(quantity);
But CreateOrderUseCase must:
load the correct Inventory
coordinate it with requested Product
persist resulting change
So:
UseCase coordinates
Inventory protects local quantity rule
Both responsibilities participate।
Rule: Customer Can Only Cancel Own Order
Order knows:
customerId
but it does not necessarily know:
who is making the current request
That information comes from authenticated request context।
Therefore ownership authorization belongs around:
CancelOrderUseCase
Example:
if (!order.customerId().equals(customerId)) {
throw new OrderAccessDeniedException();
}
Then:
order.cancel();
protects Order lifecycle।
Security Context Should Not Leak Into Domain
Bad:
public void cancel() {
Authentication authentication =
SecurityContextHolder
.getContext()
.getAuthentication();
// ...
}
inside Order।
Now domain depends on Spring Security।
This makes Order harder to test and couples business behaviour to HTTP/security infrastructure।
Better:
Security
↓
Handler / application boundary
↓
CustomerId
↓
UseCase
↓
Order
Handler Obtains Authenticated Context
Conceptually:
public OrderResponse handle(
CreateOrderRequest request,
AuthenticatedUser user
) {
return ...
}
or framework integration may provide authenticated identity another way।
Handler/application boundary converts framework security information into application-friendly values such as:
CustomerId
Then UseCase receives that value।
Do Not Trust Customer Identity From Request Body
Bad request:
{
"customerId": "customer-999",
"items": [...]
}
if authenticated identity already determines who is creating the Order।
A user could attempt to create an Order for someone else।
Instead:
Authenticated context
↓
CustomerId
is authoritative।
Client Controls Intent, Server Controls Authority
For Create Order, client can say:
I want Product P-100
quantity 2
Client should not decide:
customer owner
Order status
Product price
Order total
These are server-owned facts।
This trust boundary should be reflected in DTO design।
Request DTO Should Contain Only Client-Controlled Input
Good direction:
public record CreateOrderRequest(
List<CreateOrderItemRequest> items
) {
}
No:
status
total
customerId
unitPrice
Client does not own these values।
Price Is a Good Boundary Example
Suppose client sends:
{
"productId": "P-100",
"quantity": 2,
"unitPrice": 1
}
But Product current price is:
100
Should backend accept 1?
No।
Our design says:
server-side Product price
at successful Order creation
becomes OrderItem.unitPrice।
Therefore request DTO should not even need a price field।
Total Is Also Server-Owned
Bad:
{
"total": 1.00
}
Backend should calculate:
sum(quantity × purchase-time unit price)
through trusted domain state।
This is not merely "validation."
It is authority ownership।
Handler Should Not Calculate Total
Could Handler load Products and calculate prices before calling UseCase?
Technically possible।
But then HTTP boundary owns business workflow details।
Better:
Handler
↓
requested Product IDs + quantities
↓
CreateOrderUseCase
Then UseCase loads trusted Product state and constructs the domain।
UseCase Should Not Parse JSON
Bad:
public Order execute(
String rawJson
) {
// parse JSON
}
Now application workflow is coupled to transport representation।
Handler should parse/receive the DTO through Spring MVC।
UseCase should receive application-friendly input।
Transport Exceptions Should Not Leak Into Domain
Avoid domain methods throwing:
HttpClientErrorException
ResponseStatusException
because these represent HTTP semantics।
Domain should express business failure।
Handler/API boundary later maps that failure to appropriate HTTP response।
Business Error to HTTP Response
Conceptually:
OrderNotFound
↓
Handler/API error mapping
↓
HTTP 404
InsufficientInventory
↓
Handler/API error mapping
↓
appropriate client error response
The business layer does not need to know the numeric HTTP status code।
Why This Separation Matters
Suppose tomorrow the same UseCase is triggered by:
an administrative batch job
If UseCase throws:
new ResponseStatusException(
HttpStatus.BAD_REQUEST
);
then application logic still speaks HTTP even when no HTTP exists।
Better to express the underlying business/application failure independently।
Response DTO Mapping
Domain:
Order
might contain:
OrderId
CustomerId
status
items
createdAt
But response may expose only:
id
status
total
items
Handler maps domain/application result to DTO।
Conceptually:
return new OrderResponse(
order.id().value(),
order.status().name(),
order.total()
);
This keeps transport representation explicit।
Domain Should Not Know JSON Field Names
Avoid:
@JsonProperty("order_status")
private OrderStatus status;
inside domain solely to shape the API response।
Why?
Because now:
HTTP JSON contract
influences domain implementation।
Prefer DTO mapping where separation matters।
What About Jackson on Value Objects?
Sometimes serialization configuration for types like:
OrderId
can be convenient।
But be cautious about making the entire domain model dependent on transport annotations।
If the API representation differs from internal representation, Handler DTOs should absorb that difference।
Domain Logic Can Return Business Information
Separation does not mean Domain can only mutate state।
For example:
public boolean isPaid() {
return status == OrderStatus.PAID;
}
or:
public BigDecimal total() {
// derive total
}
are legitimate domain behaviours।
What domain should not return is something like:
HttpStatus.CONFLICT
because that belongs to transport।
UseCase Is Not a Mapper Layer
A UseCase should not spend most of its time creating HTTP DTOs।
Bad:
public OrderResponse execute(...) {
// business workflow
return new OrderResponse(...);
}
This couples application logic to HTTP response representation।
Prefer UseCase returning:
domain/application result
then Handler maps to transport DTO।
But Don't Create Result Types Without Need
If UseCase can naturally return:
Order
that's fine।
We do not automatically need:
CreateOrderResult
CreateOrderOutput
OrderApplicationModel
unless the operation needs to return information that doesn't fit a domain entity cleanly।
Separation should remain pragmatic।
Handler Example
Conceptually:
@Component
public class CreateOrderHandler {
private final CreateOrderUseCase useCase;
public CreateOrderHandler(
CreateOrderUseCase useCase
) {
this.useCase = useCase;
}
public OrderResponse handle(
CreateOrderRequest request,
CustomerId customerId
) {
Order order = useCase.execute(
customerId,
toCommand(request)
);
return toResponse(order);
}
}
Handler owns:
request → application mapping
application result → response mapping
UseCase owns workflow।
UseCase Example
Conceptually:
@Component
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;
}
public Order execute(
CustomerId customerId,
CreateOrderCommand command
) {
// validate duplicate products
// load Products
// load Inventory
// decrease Inventory
// create OrderItems
// create Order
// persist
return null;
}
}
No HTTP status codes।
No JSON parsing।
No servlet request।
Domain Example
public class Order {
private OrderStatus status;
public void cancel() {
if (status != OrderStatus.UNPAID) {
throw new IllegalStateException(
"Only unpaid orders can be cancelled"
);
}
status = OrderStatus.CANCELLED;
}
}
No Spring MVC।
No authentication framework।
No repository lookup।
The domain rule remains portable।
Repository Example
Conceptually:
public interface OrderRepository {
Optional<Order> findById(
OrderId orderId
);
Order save(
Order order
);
}
UseCase depends on persistence capability।
Handler should not normally call repository directly for business operations।
Why Handler → Repository Is Usually Wrong
Bad:
Handler
↓
Repository
for operations with business behaviour।
This skips UseCase coordination।
For example:
public void cancel(
OrderId orderId
) {
repository.updateStatus(
orderId,
CANCELLED
);
}
Now lifecycle rules can be bypassed completely।
Better:
Handler
↓
CancelOrderUseCase
↓
OrderRepository
↓
Order.cancel()
Not Every Read Needs Rich Domain Behaviour
A read-only query can sometimes be simpler।
For example Product browsing may eventually use a query-oriented repository method and return projection-like data efficiently।
We do not need to reconstruct a rich domain object for every read if no domain behaviour is required।
But this is a performance/application design decision, not a reason to collapse all boundaries।
Commands and Transport Independence
Suppose request:
public record CreateOrderRequest(
List<CreateOrderItemRequest> items
) {
}
UseCase input:
public record CreateOrderCommand(
List<Item> items
) {
}
Why separate?
Perhaps API sends Product IDs as strings, while application uses:
ProductId
Then Handler can convert:
String
↓
ProductId
before calling UseCase।
That keeps transport parsing out of the application workflow।
But Avoid Mechanical Mapping
If request and UseCase input are currently identical and no boundary value is gained, use a simpler signature।
For example:
useCase.execute(
customerId,
request.items()
);
may initially be enough।
Architecture should remove complexity, not manufacture it।
Transport-Level Validation
Examples that clearly belong to Handler/API boundary:
malformed JSON
missing request body
invalid path parameter format
unsupported content type
required request field absent
query parameter cannot be parsed
These have no meaning outside the transport protocol।
Application-Level Validation
Examples that require system coordination:
Product does not exist
same Product appears twice in request
Order does not belong to customer
Product is not eligible for current workflow
requested Inventory cannot satisfy Order
These often belong in UseCase because they involve:
repositories
authenticated context
multiple domain concepts
Domain-Level Validation
Examples:
Product price cannot be negative
Inventory quantity cannot be negative
Order Item quantity must be positive
Order must contain at least one item
PAID Order cannot be cancelled
These are intrinsic valid-state rules।
Database-Level Protection
Some invariants should also be reinforced by PostgreSQL where practical।
For example:
NOT NULL
foreign keys
CHECK inventory_quantity >= 0
This is not transport logic or domain logic।
Database constraints protect persisted state。
Four Protection Boundaries
A useful model:
Transport
↓
protects request contract
UseCase
↓
protects workflow coordination
Domain
↓
protects business state
Database
↓
protects persisted integrity
Different boundaries solve different failure modes।
Example: Negative Inventory
Transport may reject:
admin sets quantity = -5
But Inventory domain should also reject:
inventory.setAvailableQuantity(-5);
And PostgreSQL may later have:
CHECK (available_quantity >= 0)
These protections are layered because negative Inventory is a critical invariant।
Example: Duplicate Product Lines
Where should duplicate Product detection live?
Incoming DTO could technically inspect duplicates।
But duplicate Product rule is business/application behaviour, not JSON structure।
For example the same operation could be invoked by another transport later।
So primary ownership should be:
CreateOrderUseCase
and valid Order construction should not casually allow duplicate items either।
Don't Put Repository Calls in Validation Annotations
It may be tempting to write custom HTTP validator that calls:
ProductRepository
to check whether Product exists।
This mixes transport validation with application persistence。
Better:
Handler
→ basic request validation
UseCase
→ Product existence
Keep external/input validation cheap and local where possible।
Why Database-Backed Validation Belongs Deeper
Suppose request validation checks:
Product exists
and then later UseCase executes।
Between those steps, state could change।
More importantly, the workflow itself still needs to load Product।
So performing repository-backed "validation" in Handler often duplicates work and splits consistency logic।
UseCase should make decisions using the state it actually operates on।
Validation vs Business Operation
Avoid thinking:
validate everything first
then business logic
as two completely separate phases।
Sometimes validation is the business operation।
For example:
inventory.decrease(quantity);
checks sufficient Inventory and performs the valid state change together।
This reduces:
check then mutate separately
logic that callers might misuse।
Tell, Don't Ask
Weak:
if (inventory.quantity() >= amount) {
inventory.setQuantity(
inventory.quantity() - amount
);
}
Caller asks state, decides rule, mutates object।
Better:
inventory.decrease(amount);
The object owns the rule।
This is an example of the OOP principle often described as:
Tell, don't ask.
Use it pragmatically, not dogmatically।
UseCase Still Needs to Ask Sometimes
For example Product browse may need:
product.isActive()
combined with:
inventory.quantity() > 0
to determine customer-visible availability।
This is a cross-concept decision।
A UseCase/query workflow can reasonably inspect both states।
Not every getter is a design failure।
Transport Logic Should Be Replaceable
Imagine future system exposes:
REST API
and perhaps an internal command interface।
If domain and UseCases do not depend on HTTP, both can reuse:
CreateOrderUseCase
Only the incoming Handler differs।
Conceptually:
REST Handler ───┐
├── CreateOrderUseCase
Other Handler ──┘
This is one practical benefit of separation।
Domain Tests Need No HTTP
Testing cancellation:
@Test
void paidOrderCannotBeCancelled() {
Order order = unpaidOrder();
order.markPaid();
assertThrows(
IllegalStateException.class,
order::cancel
);
}
No:
MockMvc
JSON
ApplicationContext
needed।
This gives fast, focused feedback।
UseCase Tests Need No HTTP Either
Example:
CreateOrderUseCase useCase =
new CreateOrderUseCase(
productRepository,
inventoryRepository,
orderRepository
);
Then call:
useCase.execute(
customerId,
command
);
We can verify business coordination without invoking HTTP।
Handler Tests Focus on Transport
Handler/API tests should focus on things such as:
request validation
JSON mapping
HTTP status
response shape
authentication boundary
They should not be the only place where Order lifecycle is tested।
Avoid Re-testing Entire Business Logic Through HTTP
Suppose Order has ten lifecycle rules।
Testing all ten only through full HTTP tests makes failures slower and harder to diagnose।
Better:
Domain tests
→ domain invariants
UseCase tests
→ workflow coordination
Handler/API tests
→ transport contract
This gives a balanced test strategy।
Error Translation
Eventually we may have domain/application failures such as:
OrderNotFoundException
OrderNotCancellableException
InsufficientInventoryException
Handler-level error translation may map them to API responses।
Example conceptually:
OrderNotFound
↓
404 Not Found
invalid request
↓
400 Bad Request
Exact error contract comes in REST API lessons।
Domain Error Should Not Carry HTTP Metadata
Avoid:
public class InsufficientInventoryException
extends RuntimeException {
private final int httpStatus = 409;
}
Now business failure knows transport protocol।
Keep error meaning separate from API representation।
Same Business Failure, Different Transport
Today:
HTTP
may map failure to status code।
Tomorrow a message-processing system might:
reject message
retry
send to dead-letter queue
Same domain failure, different transport response।
This is why domain should express meaning rather than HTTP mechanics।
What About Logging?
Handler may log request context carefully।
UseCase may produce meaningful application logs later।
Domain methods should not generally log every business action themselves।
For example:
order.cancel();
should not require logger infrastructure to function।
Logging is a cross-cutting operational concern, covered later।
What About Metrics?
Similarly:
order.cancel()
should not call a metrics backend directly।
UseCase/application/instrumentation boundary can record operational metrics later।
Keep domain behaviour independent।
What About Transactions?
CreateOrderUseCase coordinates:
Inventory decrease
Order persistence
so transaction boundary later belongs around the UseCase operation।
Handler should not decide database transaction details।
Domain should not call transaction APIs।
Again, each responsibility has a home।
What About External Services?
Payment is a useful example।
Flow:
PayOrderHandler
↓
PayOrderUseCase
↓
PaymentService
PaymentService handles provider protocol।
PayOrderUseCase handles application workflow।
Order handles lifecycle transition।
Payment Example Separation
Provider returns:
{
"status": "CAPTURED",
"reference": "PAY-123"
}
ProviderPaymentService maps this into application-facing:
PaymentResult
Then UseCase decides:
confirmed success
↓
order.markPaid()
Order never sees provider JSON।
Handler never parses provider response।
Transport Types Should Stop at the Boundary
A useful rule:
HTTP request/response types
should generally stop around Handler.
Similarly:
Provider request/response types
should stop around external Service.
The middle of the application should use its own meaningful types।
Avoid HttpServletRequest in UseCase
Bad:
public Order execute(
HttpServletRequest request
) {
}
Now UseCase needs servlet infrastructure।
Better:
public Order execute(
CustomerId customerId,
CreateOrderCommand command
) {
}
Application input is explicit and framework-independent।
Avoid Spring MVC Annotations in Domain
Bad:
public class Order {
@RequestParam
private String status;
}
Clearly wrong responsibility।
Less obvious versions, such as JSON-specific annotations everywhere in domain, deserve the same scrutiny।
Response Formatting Belongs Outside Domain
Suppose API wants:
"paid"
instead of:
PAID
Domain can keep:
OrderStatus.PAID
Handler/serialization mapping handles API representation।
Do not weaken domain naming merely to make JSON pretty।
Business Language Can Differ From API Language
Domain may use:
UNPAID
while public API eventually uses:
pending_payment
if product contract decides so।
DTO mapping provides translation।
This prevents public representation from becoming internal architecture vocabulary accidentally।
Avoid Leaking Persistence Models Into Handlers
Bad:
public OrderEntity handle(...) {
}
if OrderEntity is a persistence-specific representation।
Then Handler is coupled directly to database mapping।
Better flow:
Handler
↓
UseCase
↓
Domain/Application result
Repository owns persistence representation।
A Complete Create Order Flow
Conceptually:
HTTP Request
↓
CreateOrderRequest
↓
CreateOrderHandler
Handler:
validate transport shape
obtain authenticated CustomerId
convert input
Then:
CreateOrderUseCase
UseCase:
reject duplicate Product IDs
load Products
load Inventory
coordinate availability
capture current prices
decrease Inventory
construct OrderItems
construct Order
persist state
Domain:
Product
→ valid price/state
Inventory
→ valid quantity change
OrderItem
→ positive quantity/historical price
Order
→ valid item collection/lifecycle/total
Then:
Order
↓
Handler
↓
OrderResponse
↓
HTTP Response
Responsibilities stay visible।
A Complete Cancel Order Flow
Incoming:
DELETE /orders/{id}
or whatever API contract we choose later।
Handler:
parse Order ID
obtain authenticated CustomerId
UseCase:
load Order
verify ownership
call order.cancel()
restore Inventory
persist changes
Domain:
Order.cancel()
→ reject PAID
→ reject already CANCELLED
→ transition UNPAID → CANCELLED
Handler:
map success/failure to HTTP response
No layer owns all responsibilities।
Why Ownership Matters
Suppose we put cancellation status check only in Handler।
Then another internal caller could bypass it।
Suppose we put current-customer ownership inside Order।
Then Order must know runtime authenticated user context।
Suppose we put Inventory restoration inside Order.cancel()।
Then Order becomes coupled to Inventory infrastructure/state it doesn't own।
Each rule belongs where its required information and responsibility intersect।
A Practical Decision Framework
When writing logic, ask these questions in order:
1. Is this about HTTP/JSON/request representation?
Then likely:
Handler
2. Does it require coordinating multiple domain concepts, repositories, authentication context, or external systems?
Then likely:
UseCase
3. Can the rule be decided entirely using one domain object's owned state?
Then likely:
Domain
4. Is it about storing or retrieving state?
Then:
Repository
5. Is it about communicating with a third-party system?
Then:
Service
This framework will solve most placement questions।
Avoid "Business Logic" as One Giant Category
People often say:
Business logic goes in Service.
That is too vague for our architecture।
Different business responsibilities belong in different places।
For example:
Order lifecycle rule
→ Domain
Create Order coordination
→ UseCase
HTTP request validation
→ Handler
Our terminology makes that explicit।
Avoid Thin Domain + Giant UseCase
Even with good Handler separation, a UseCase can still become too procedural।
Bad:
if (order.status() != UNPAID) {
...
}
order.setStatus(CANCELLED);
Better:
order.cancel();
UseCase coordinates; domain owns local rules।
Avoid Giant Domain + Empty UseCase
Opposite bad design:
order.cancel(
customerId,
orderRepository,
inventoryRepository,
paymentService
);
Now Order coordinates the entire application।
UseCase would become pointless।
Domain should not own repositories or external Services।
Handler Should Be Thin, Not Empty Ceremony
"Thin Handler" does not mean Handler must only contain one line।
It may legitimately handle:
path/request mapping
authenticated context
transport validation
response mapping
That's real work।
The goal is not minimum line count।
The goal is correct responsibility।
UseCase Should Be Explicit, Not Generic
Prefer:
CreateOrderUseCase
CancelOrderUseCase
over:
OrderUseCase
with a dozen unrelated methods if operations have distinct workflows।
This makes dependencies and responsibilities clearer।
Domain Should Be Expressive, Not Framework-Aware
Good domain code should read like:
inventory.decrease(quantity);
order.cancel();
order.markPaid();
product.deactivate();
rather than:
orderEntity.setStatus(...);
repository.save(...);
httpResponse.setStatus(...);
inside one object।
Transport Independence Helps Refactoring
Suppose later API version changes request shape।
Old:
{
"productId": "P-1",
"quantity": 2
}
New:
{
"items": [
{
"product": "P-1",
"qty": 2
}
]
}
If UseCase receives application-friendly input, only Handler/DTO mapping may need substantial change।
Domain and workflow can remain stable।
Domain Independence Helps Persistence Refactoring
Likewise, if database implementation changes internally, Handler should not care।
Flow remains:
Handler
↓
UseCase
↓
Repository
Repository isolates persistence mechanics।
Separation at one boundary supports separation at others।
Common Mistake 1 — Controller/Handler Does Everything
HTTP class becomes workflow + domain + repository layer।
Result:
hard to test
hard to reuse
business rules easy to bypass
Common Mistake 2 — Domain Object Used as Request DTO
Client gets control over fields it should not own।
Common Mistake 3 — UseCase Throws HTTP Exceptions
Application layer becomes tied to REST।
Common Mistake 4 — Domain Reads Security Context
Business object becomes Spring Security-dependent।
Common Mistake 5 — Handler Calls Repository Directly for Mutating Workflows
UseCase/domain rules can be bypassed।
Common Mistake 6 — Database Lookup in Request Validator
Transport validation becomes persistence-aware and duplicates workflow state access।
Common Mistake 7 — Provider DTO Passed Into Domain
External protocol contaminates business model।
Common Mistake 8 — Client Price/Total Trusted
Server loses authority over important business values।
Common Mistake 9 — Every Validation Repeated Everywhere
Rules should be layered deliberately, not copied mechanically।
Common Mistake 10 — All Rules Put in UseCase
Entities become passive data bags instead of protecting state they own।
Review Checklist
When reviewing a Handler:
Does it mostly deal with transport and mapping?
Is it calling a UseCase rather than implementing
the whole business workflow?
Is it trusting client-controlled fields
that should be server-owned?
Does it call repositories directly without reason?
When reviewing a UseCase:
Does it coordinate one meaningful operation?
Are HTTP/JSON types leaking into it?
Does it own repository/external Service coordination?
Is it manually implementing rules
that belong to domain objects?
When reviewing domain code:
Does it protect its own invariants?
Does it depend on Spring MVC, repositories,
security context, or external clients?
Can it be tested with plain Java?
Engineering Principle
The core principle:
Transport describes how a request enters or leaves the system; UseCases coordinate what the application must do; domain objects protect the business state they own.
Another:
Move external protocol concerns toward the edges and business meaning toward the center.
And:
Do not trust transport input merely because it reached a valid DTO—server-owned business facts must still come from trusted application/domain state.
Summary
In this lesson, we learned that:
- Transport logic and domain logic solve different problems.
- Handler owns HTTP/JSON/request-response concerns.
- UseCase owns application workflow coordination.
- Domain objects own local business rules and valid state.
- Repository owns persistence concerns.
- Service represents third-party/external integration.
- Request DTOs represent untrusted external input.
- Clients should not control Order ownership, status, authoritative price, or total.
- Handler-level validation protects request shape.
- UseCase-level validation handles cross-entity and system coordination.
- Domain validation protects intrinsic invariants.
- Database constraints may reinforce critical persisted integrity.
- Validation across multiple boundaries can be legitimate when each layer protects a different guarantee.
- Domain rules should not depend on Spring MVC or Spring Security.
- Authenticated identity should be translated into application-friendly values such as
CustomerId. - UseCases should not parse JSON or throw HTTP-specific errors.
- Domain errors should be translated into HTTP responses at the API boundary.
- Handlers should map request DTOs inward and application/domain results outward.
- Domain entities should not be used directly as request bodies merely for convenience.
- Provider-specific DTOs should stay inside the external Service boundary.
- UseCases should coordinate cross-capability behaviour while domain objects protect state they own.
- Handler → Repository shortcuts can bypass important workflow/domain rules.
- Domain tests and UseCase tests should not require HTTP.
- Handler/API tests should focus on transport behaviour.
- Good separation keeps business behaviour reusable and framework-independent.
Next lesson:
Modeling Product
There we will take the Product concept from our RFC and turn it into a concrete Java domain model, including identity, current price, active/inactive state, construction invariants, and meaningful Product behaviour without mixing in Inventory or persistence concerns.