Domain Modeling

Separating Domain Logic from Transport Logic

ReadingPreview

আপনি একটি 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.