Domain Modeling

Entities, Value Objects, and DTOs

ReadingPreview

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

আগের lesson-এ আমরা business concepts থেকে domain model তৈরি করার foundation দেখেছি।

আমাদের current concepts:

Product
Inventory
Order
OrderItem
CustomerId
ProductId
OrderId

এখন প্রশ্ন:

এগুলোর প্রত্যেকটি কি Entity?

না।

Backend application-এ সাধারণত আমরা তিন ধরনের object frequently encounter করি:

Entity

Value Object

DTO

তিনটিই data carry করতে পারে।

তিনটিই Java class বা record হতে পারে।

কিন্তু তাদের meaning এবং responsibility সম্পূর্ণ আলাদা

এই distinction ঠিকভাবে বুঝতে পারলে:

domain design clearer হয়

unnecessary setters কমে

identity handling improve হয়

API model domain-এর সঙ্গে accidentally couple হয় না

JPA design business model dictate করতে পারে না

The Core Difference

সবচেয়ে simple mental model:

Entity
→ identity matters
Value Object
→ value matters
DTO
→ data transfer matters

এখন এগুলো একে একে বুঝি।


What Is an Entity?

একটি Entity এমন business object যার identity সময়ের সঙ্গে important।

তার state change হতে পারে, কিন্তু আমরা সেটিকে একই object হিসেবে চিনতে থাকি।

Example:

Order #ORD-1001

Initially:

status = UNPAID

Later:

status = PAID

State changed।

কিন্তু এটি এখনও একই:

Order #ORD-1001

So Order is clearly an Entity।


Entity Equality Is About Identity

Suppose:

Order A
id = 1001
status = UNPAID
total = 50

and:

Order B
id = 1001
status = PAID
total = 50

State differs।

But conceptually both represent:

the same Order

because identity is the same।

Entity identity survives state changes।


Product Is Also an Entity

Consider:

Product #P-42

Today:

name = Mechanical Keyboard
price = 100

Next month:

price = 90

Product changed।

But we still mean the same Product।

Therefore Product is also a clear Entity।


What Makes Something an Entity?

Useful questions:

Does this concept have an identity
that matters independently of its current values?

Does it change over time?

Do we need to refer to the same instance
across multiple operations?

Would two objects with identical fields
still represent different business things?

If yes, Entity is likely appropriate।


Example: Two Identical Products

Suppose two records both contain:

name = "Keyboard"
price = 100
active = true

But IDs:

Product #100
Product #200

They are still two different Products।

Their values happen to match।

Identity determines which Product we mean।


Entity Is Not the Same as JPA @Entity

This distinction is critical।

DDD/domain terminology:

Entity
→ business identity matters

JPA terminology:

@Entity
→ class is mapped through JPA persistence

They frequently overlap।

But they are not the same concept।


Domain Entity Can Exist Without JPA

Example:

public class Order {

    private final OrderId id;
    private OrderStatus status;

    public void cancel() {
        // domain rule
    }
}

This is already a domain Entity conceptually।

We do not need:

@Entity

to make it one।

Persistence annotations answer a separate question:

How will this object be stored?


JPA Entity Does Not Automatically Mean Good Domain Entity

You could write:

@Entity
public class Something {

    @Id
    private Long id;

    private String value;
}

That makes it a JPA-managed persistent type।

It does not prove that:

identity has meaningful business semantics

business invariants are protected

domain behaviour is correctly modeled

ORM annotation is not domain design।


Entity State Should Be Protected

Because Entity state changes over time, we should control meaningful transitions।

For Order:

order.markPaid();

instead of:

order.setStatus(PAID);

For Product:

product.changePrice(newPrice);

instead of unrestricted:

product.setPrice(newPrice);

Entity is not just:

ID + mutable fields

It should protect the rules around its state।


Identity Should Usually Be Stable

A Product can change:

name

price

active state

but its identity should not arbitrarily change from:

ProductId(10)

to:

ProductId(20)

The same applies to Order।

Entity identity normally remains stable throughout its lifecycle।


What About Inventory?

Inventory is interesting।

Our v1 model says:

one Inventory state per Product

and Inventory owns:

available quantity

We need to repeatedly find and update the same Inventory state for a Product।

Conceptually its identity can be:

ProductId

rather than requiring a separate:

InventoryId

So Inventory behaves like an Entity/stateful domain concept:

Inventory for Product #42

Its quantity can change:

10 → 8 → 5 → 9

while it remains the Inventory state associated with that Product।


Entity Does Not Require a Dedicated ID Class

An Entity needs meaningful identity।

That does not mean every Entity needs:

InventoryId

For Inventory, this may be enough:

public class Inventory {

    private final ProductId productId;
    private int availableQuantity;
}

ProductId identifies which Inventory state we mean।

Don't create IDs purely because database tables often have primary keys।


What About OrderItem?

OrderItem is more subtle।

It belongs inside an Order:

Order
    ├── OrderItem
    └── OrderItem

Current requirements do not need:

GET /order-items/{id}

update one OrderItem independently

track OrderItem lifecycle independently

The customer thinks about:

Order

not an independently managed Order Item resource।


Does OrderItem Need Independent Identity?

Ask:

If two Order Items have the same Product, quantity, and purchase-time price, do we need to distinguish them independently?

Our current rule already says:

same Product cannot appear twice
in one Order

So there is little business need for independent OrderItemId right now।

That means we should not invent domain identity for OrderItem just because the database may later have a row ID


OrderItem Can Be Modeled as an Owned Child

For v1, think of OrderItem as:

a component owned by Order

with values such as:

ProductId

quantity

unitPrice

Its lifecycle follows Order।

Whether persistence later gives the row a technical primary key is a separate concern।


Is OrderItem a Value Object Then?

It has strong value-like characteristics:

no independent lifecycle

no independently meaningful identity

immutable purchase-time facts

owned by Order

So modeling it as a value-like domain object is reasonable।

Example:

public record OrderItem(
        ProductId productId,
        int quantity,
        BigDecimal unitPrice
) {
}

However, if later requirements introduce independently meaningful Order Item identity or behaviour, the model can evolve।

The important rule is:

Do not assign Entity identity unless the domain actually needs identity.


What Is a Value Object?

A Value Object is identified by the values it contains rather than by independent identity।

Example:

ProductId(42)

Two:

new ProductId(42)

objects represent the same value conceptually।

We do not care which Java instance was created।

We care that both mean:

Product identifier 42

Value Equality

Suppose:

ProductId a = new ProductId(42);
ProductId b = new ProductId(42);

Conceptually:

a == same business value as b

even though they may be separate Java objects।

Value Objects are about:

meaning of the contained value

not object identity in memory।


Value Objects Are Usually Immutable

A strong Value Object usually does not change after creation।

For example:

public record ProductId(
        long value
) {
}

Once:

ProductId(42)

exists, we do not mutate it into:

ProductId(50)

We create another value instead।

Immutability makes Value Objects easier to reason about।


Why Wrap a Primitive?

Why not use:

Long

everywhere?

Suppose:

void cancelOrder(
        Long orderId,
        Long customerId
) {
}

Someone could accidentally call:

cancelOrder(
        customerId,
        orderId
);

Compiler cannot help because both are Long


Typed IDs

With:

public record OrderId(long value) {
}

and:

public record CustomerId(String value) {
}

signature becomes:

void cancelOrder(
        OrderId orderId,
        CustomerId customerId
) {
}

Now accidental swapping becomes much harder।

The types communicate intent।


Are Typed IDs Required?

No।

Don't mechanically wrap every primitive।

Introduce a Value Object when it gives meaningful value such as:

stronger type safety

domain validation

clear semantics

repeated domain behaviour

Typed IDs are often useful in a moderately complex backend, but they still have code cost।


Value Object Can Protect Validation

Example:

public record ProductId(long value) {

    public ProductId {
        if (value <= 0) {
            throw new IllegalArgumentException(
                    "Product ID must be positive"
            );
        }
    }
}

Now any valid ProductId carries an invariant।

Caller does not need to repeatedly check the same rule।


But Don't Over-Validate Technical IDs

If ID generation strategy allows values we have not yet defined, don't invent validation such as:

ID must always be positive

without confirming the ID model।

This lesson focuses on the Value Object pattern, not choosing our final ID strategy।


Value Object Example: Money

Money is a common Value Object candidate because amount semantics can include:

precision

rounding

currency

For example:

Money(20.00, EUR)

But our v1 has no multi-currency requirement।

We currently can reasonably use:

BigDecimal

for price while enforcing non-negative values at the domain boundary।

We don't need to build a Money abstraction just to demonstrate Value Objects।


Value Object Example: Quantity

Likewise:

Quantity

could protect:

quantity > 0

But if quantity only appears in a few simple places:

int

with strong domain validation may be enough।

Domain modeling is not a competition to remove all primitives।


Good Value Object Candidates

Typical candidates have one or more of these characteristics:

strong semantic meaning

repeated validation

multiple fields that belong together

value-based equality

immutability

useful behaviour around the value

Examples might include:

OrderId

ProductId

CustomerId

Money

depending on system requirements।


Entity Can Contain Value Objects

Example:

public class Order {

    private final OrderId id;
    private final CustomerId customerId;
    private OrderStatus status;
}

Here:

Order
→ Entity

while:

OrderId
CustomerId
→ Value Objects

This is normal।

Value Objects help express Entity state more precisely।


Value Objects Can Contain Behaviour

Value Object does not mean "just fields."

Example conceptually:

public record Money(
        BigDecimal amount
) {

    public Money add(Money other) {
        return new Money(
                amount.add(other.amount)
        );
    }
}

Because it is immutable:

add()

returns a new value rather than mutating existing state।


Entity Mutation vs Value Object Replacement

For Entity:

product.changePrice(newPrice);

mutates state of the same Product।

For immutable Value Object:

Money updated =
        oldMoney.add(other);

returns a new value।

This reflects their different identities।


What Is a DTO?

DTO means:

Data Transfer Object

Its primary purpose is:

Move data across a boundary.

In our backend, common DTO boundaries include:

HTTP request → Handler

Handler → HTTP response

Example:

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

This describes incoming API data।

It is not our Order domain entity।


DTO Exists for Communication

Suppose incoming JSON:

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

A request DTO may represent that shape:

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

Its responsibility:

represent incoming transport data

not:

protect the entire Order domain lifecycle

DTO Is Not an Entity

A CreateOrderRequest does not have meaningful long-lived identity।

We do not say:

CreateOrderRequest #1023

and track it across months।

It exists temporarily to transfer request data।

Therefore it is not a domain Entity।


DTO Is Not Automatically a Value Object

A DTO may be immutable and have value-based equality if implemented as a Java record

But architecturally, its responsibility is still transfer।

For example:

public record OrderResponse(
        String id,
        String status,
        BigDecimal total
) {
}

may look value-like technically।

But its reason for existing is:

API response representation

So we classify it by architectural responsibility as a DTO।


Java Shape Does Not Define Domain Role

All three could be Java records:

record ProductId(...)

record CreateOrderCommand(...)

record OrderResponse(...)

But they have different responsibilities।

Therefore don't classify objects only by syntax।

Ask:

Why does this type exist?


DTOs Belong Near the Boundary

Our architecture:

HTTP
    ↓
Handler
    ↓
UseCase

Request/response DTOs belong near:

Handler

For example:

order/
└── handler/
    ├── CreateOrderHandler.java
    ├── CreateOrderRequest.java
    └── CreateOrderResponse.java

They should not automatically live in:

order/domain/

Don't Use Domain Entities as Request DTOs

Tempting:

@PostMapping("/orders")
Order create(
        @RequestBody Order order
) {
}

This is problematic।

Now the client may control fields such as:

Order ID

status

customer ownership

purchase-time price

total

which should be server-controlled।

Transport model and domain model represent different trust boundaries।


Client Input Is Untrusted

For Create Order, client should provide something like:

Product ID

quantity

The server derives:

authenticated CustomerId

current Product price

Order status

Order total

Therefore incoming DTO should not look like complete Order state।


Bad CreateOrderRequest

public record CreateOrderRequest(
        String customerId,
        String status,
        BigDecimal total,
        List<OrderItem> items
) {
}

This gives client authority over server-owned facts।


Better Request Shape

Conceptually:

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

and:

public record CreateOrderItemRequest(
        String productId,
        int quantity
) {
}

Customer identity comes from authenticated context।

Current price comes from Product state।

Status is established by domain rules।

Total is calculated by backend।


DTO Should Reflect the Contract

DTO should represent:

what this boundary accepts or returns

not:

every field that happens to exist on the database entity

This keeps API contract intentional।


Response DTOs

Suppose Order contains internal information that should not be exposed।

The API can return:

public record OrderResponse(
        String id,
        String status,
        BigDecimal total
) {
}

without exposing every internal field।

The API contract can evolve separately from domain implementation।


Why Not Return Domain Object Directly?

If Handler serializes Order directly:

Domain structure
        =
API structure

Now changing domain internals may accidentally change public API।

Example:

add internal persistence field

could unexpectedly appear in JSON depending on serialization configuration।

Explicit response DTO gives control।


DTOs Protect Boundaries

Using dedicated DTOs provides a place to define:

incoming fields

outgoing fields

transport validation

serialization format

without forcing domain model to serve HTTP concerns।


DTOs Should Usually Contain Data, Not Business Logic

A request DTO might contain transport-level validation metadata later।

But it should not become:

request.cancelOrder();

or:

request.calculateOrderTotal();

Those behaviours belong to application/domain responsibilities।

DTOs are primarily boundary data carriers।


DTOs Are Often Short-Lived

Lifecycle:

HTTP request arrives
    ↓
DTO created
    ↓
Handler reads DTO
    ↓
mapped to UseCase input
    ↓
DTO discarded

This is very different from Entity lifecycle:

Order created
    ↓
persisted
    ↓
loaded later
    ↓
state changes
    ↓
persisted again

What About Commands?

Our Handler may convert request DTO into something like:

public record CreateOrderCommand(
        List<Item> items
) {
}

Then:

Handler
    ↓
CreateOrderCommand
    ↓
CreateOrderUseCase

Is CreateOrderCommand a DTO?

Broadly, it transfers data between application components।

But for clarity in our codebase we can call it:

UseCase input / Command

because its role is more specific than generic DTO terminology।


Request DTO vs Command

Consider:

CreateOrderRequest

represents:

HTTP contract

while:

CreateOrderCommand

represents:

application operation input

They may initially contain similar values।

But they belong to different boundaries।


Do We Always Need Both?

No।

If mapping gives no useful separation, Handler can call:

createOrderUseCase.execute(
        customerId,
        request.items()
);

directly।

Do not create:

Request
Command
Input
Payload
Model

all carrying identical data just to satisfy architecture diagrams।


When Command Separation Becomes Useful

A separate UseCase input model is useful when:

HTTP representation differs from application input

multiple Handlers invoke same UseCase

authentication context must be combined with request data

transport-specific types should not leak inward

input becomes complex enough to deserve a name

Introduce it because it clarifies the boundary।


DTO Mapping

Typical flow:

CreateOrderRequest
        ↓
Handler
        ↓
CreateOrderCommand
        ↓
CreateOrderUseCase

Then:

Domain Order
        ↓
Handler mapping
        ↓
OrderResponse

Mapping can be simple Java code।

We do not need a mapping framework automatically।


Avoid Mapper Ceremony

For a three-field response:

return new OrderResponse(
        order.id().value(),
        order.status().name(),
        order.total()
);

may be perfectly readable।

No immediate need for:

OrderMapper

OrderDtoMapper

OrderResponseMapper

unless mapping complexity or reuse justifies it।


Entity, Value Object, DTO Together

Consider the future Create Order flow.

Incoming:

CreateOrderRequest

is a DTO।

Handler obtains:

CustomerId

which may be a Value Object।

UseCase loads:

Product

which is an Entity।

It works with:

Inventory

which is stateful identity-bearing domain state।

It creates:

OrderItem

which can be modeled as an owned value-like child।

Then creates:

Order

which is an Entity।

Finally Handler returns:

OrderResponse

which is a DTO।


Visual Model

HTTP
 │
 ▼
CreateOrderRequest
[DTO]
 │
 ▼
CreateOrderHandler
 │
 ▼
CustomerId
[Value Object]
 │
 ▼
CreateOrderUseCase
 │
 ├── Product
 │   [Entity]
 │
 ├── Inventory
 │   [Entity-like state]
 │
 ├── OrderItem
 │   [owned value-like object]
 │
 └── Order
     [Entity]
 │
 ▼
OrderResponse
[DTO]

Each type exists for a different reason।


Entity Identity and Database IDs

Suppose PostgreSQL table later contains:

order_items.id

Does that automatically make OrderItem a domain Entity?

No।

Database may need a technical key for:

primary key

foreign key reference

ORM mapping

But domain identity asks:

Does the business need to distinguish this Order Item independently over time?

Those are different questions।


Database Row != Domain Entity

Similarly, a table:

payment_attempts

might eventually contain rows with IDs।

That does not automatically mean each row should become a rich domain Entity।

Persistence structure should not determine domain taxonomy blindly।


Value Object != Database Embedded Type

Likewise, calling something a Value Object does not require a specific JPA mapping such as:

@Embeddable

That is a persistence choice।

Domain classification comes first।


DTO != API Only

DTOs are frequently used for HTTP, but the concept can apply to other boundaries:

message queue payload

external Service request/response

file import structure

In our current course, HTTP and third-party integration are the relevant examples।


External Service DTOs

Payment provider may have:

ProviderPaymentRequest
ProviderPaymentResponse

These are DTOs for the provider protocol।

They belong near:

payment/service/

or its integration-specific structure।

They should not leak into:

order/domain/

Map Provider DTOs at the Boundary

External provider:

ProviderPaymentResponse

could contain:

CAPTURED
DECLINED
providerReference
raw provider fields

External Service should map that into an application-facing result such as:

PaymentResult

Then PayOrderUseCase does not depend on provider-specific DTOs।


Value Object Validation vs DTO Validation

Suppose incoming request:

productId = ""

Handler/transport validation can reject malformed input।

If application converts it to:

ProductId

the Value Object may also protect its own valid representation।

Different boundaries can enforce appropriate rules।


DTO Can Be Invalid From Domain Perspective

That's okay।

A DTO represents untrusted input।

Example:

new CreateOrderItemRequest(
        "42",
        -5
);

may exist briefly after deserialization।

Then validation rejects it।

A valid domain object should have stronger guarantees।


Domain Objects Should Have Stronger Guarantees Than DTOs

Useful mental model:

DTO
→ may contain untrusted external data
UseCase input
→ should be better structured
Domain object
→ should protect important invariants

As data moves inward, trust should become stronger।


Don't Let DTO Validation Become the Only Protection

Suppose Handler validates:

quantity > 0

Should OrderItem accept negative quantity because HTTP already checked it?

No।

Another internal caller or test could create an OrderItem directly।

If positive quantity is a domain invariant, domain should protect it too।


But Don't Put HTTP Rules in Domain

Example:

JSON property must be named productId

is not domain behaviour।

Neither is:

HTTP field required annotation

Domain only cares about meaningful business state।


Equality Rules

Entity and Value Object equality deserve careful distinction।

Entity:

same ID
→ conceptually same Entity

Value Object:

same contained values
→ conceptually same value

DTO:

equality is usually not central to its domain meaning

though Java record may provide structural equality conveniently।


Java record and Value Objects

Records are often a good fit for immutable Value Objects:

public record ProductId(
        long value
) {
}

because Java automatically gives:

final components

value-based equals/hashCode

compact representation

But:

record does not automatically make something a Value Object.

Its architectural meaning still matters।


Java record and DTOs

Records are also useful for DTOs:

public record OrderResponse(
        String id,
        String status,
        BigDecimal total
) {
}

Same Java language feature।

Different architectural responsibility।


Should Entities Be Records?

Usually not when they have meaningful mutable lifecycle।

Order changes:

UNPAID → PAID

Product price changes।

Inventory quantity changes।

Java records are designed around shallowly immutable data carriers, so regular classes are usually more natural for these Entities।


Entity Mutation Should Still Be Controlled

Regular class does not mean:

public setters everywhere

Use explicit business methods।

Example:

public class Product {

    private BigDecimal price;
    private boolean active;

    public void changePrice(
            BigDecimal newPrice
    ) {
        // validate
        this.price = newPrice;
    }

    public void deactivate() {
        active = false;
    }
}

Value Objects Help Reduce Primitive Confusion

Consider:

public Order find(
        long id
) {
}

What does id represent?

Could be:

Order ID

Customer ID

Product ID

Typed value:

public Order find(
        OrderId orderId
) {
}

makes API more expressive।


But Avoid Wrapper Explosion

This:

ProductName
ProductDescription
ProductStatus
ProductPrice
InventoryQuantity
OrderCreatedAt
OrderItemQuantity

may make a small system much harder to navigate if each type only wraps one primitive without meaningful behaviour।

Use Value Objects strategically।


A Useful Threshold for Value Objects

Consider creating one when at least one is true:

wrong primitive can easily be passed

validation is repeated

multiple values form one concept

special operations belong to the concept

the value appears across important boundaries

Not because:

DDD book says primitives are bad

DTO Versioning and Domain Stability

Suppose API response changes from:

{
  "status": "UNPAID"
}

to:

{
  "status": "unpaid"
}

That is transport representation。

We should not need to rename:

OrderStatus.UNPAID

inside the domain just to satisfy JSON formatting।

DTO mapping provides that separation।


Domain Change Without API Change

Likewise, internal domain may later introduce a richer type:

OrderTotal

while API continues to return:

{
  "total": 50.00
}

DTO protects public contract from internal refactoring।


API Change Without Domain Change

We may later return:

createdAt

items

pagination metadata

without changing Order's core business rules।

Again:

Domain model
≠
API contract

Persistence Model Is Another Separate Concern

Eventually we may have:

Domain Order

Persistence Order representation

API OrderResponse

Do we always need three separate classes?

No।

But conceptually these are three responsibilities:

business behaviour

database mapping

transport representation

We separate actual classes where doing so provides enough value to justify the mapping cost।


Avoid One Object for Every Layer by Default

Over-engineered:

OrderEntity
OrderDomain
OrderDto
OrderResponse
OrderCommand
OrderModel
OrderView

for a simple operation।

We want separation where boundaries matter, not object multiplication as a ritual।


Start With Meaningful Distinctions

For our project, strong distinctions are:

Request/Response DTO
≠
Domain Entity

and:

External provider DTO
≠
Application/domain model

Persistence/domain separation can be decided pragmatically in the JPA module।


Entity Lifecycle

Entities often have lifecycle behaviour।

Order:

create
    ↓
UNPAID
    ├── markPaid()
    │       ↓
    │      PAID
    │
    └── cancel()
            ↓
        CANCELLED

This lifecycle is one of the reasons Order identity matters।


Value Object Lifecycle

A Value Object typically doesn't have identity-based lifecycle।

For example:

ProductId(42)

doesn't become:

ProductId(43)

We simply have another value।


DTO Lifecycle

DTO lifecycle is usually boundary-scoped:

deserialize
    ↓
use
    ↓
discard

or:

construct response
    ↓
serialize
    ↓
discard

These distinctions influence design decisions।


Entity Ownership

Order Entity owns Order Items।

That means callers should not independently modify them without Order's rules।

For example, this would be dangerous:

order.items().add(...);

because the internal collection can be modified externally।

Order should protect its aggregate state।


Value-Like OrderItem Fits Ownership

If OrderItem is immutable:

public record OrderItem(
        ProductId productId,
        int quantity,
        BigDecimal unitPrice
) {
}

Order can safely contain immutable item values।

That aligns well with our v1 behaviour where order lines do not change independently after successful Order creation।


Should Order Items Change After Order Creation?

Current requirements do not include:

add item to existing Order

remove item from existing Order

change item quantity after creation

Therefore immutable OrderItem is a good conceptual fit।

Don't add mutation APIs for behaviours we don't support।


Historical Price Strengthens Immutability

unitPrice represents:

purchase-time price

If someone can later call:

orderItem.setUnitPrice(...)

we could corrupt Order history।

So immutable OrderItem strongly fits the requirement।


Entity References Through Value Objects

Instead of:

OrderItem(Product product)

we may use:

OrderItem(ProductId productId, ...)

because Order Item needs historical Product reference, not ownership of live Product state।

This reduces accidental coupling between Order aggregate and current Product object।


DTO Should Not Expose Internal Entity Reference Types Automatically

Suppose domain uses:

ProductId

API may serialize it as:

{
  "productId": "42"
}

Handler mapping owns representation।

Domain should not need JSON annotations simply to satisfy transport formatting where avoidable।


Keep Jackson Out of Domain Where Practical

Avoid making domain types responsible for:

JSON field names

serialization quirks

API formats

just because Spring MVC uses Jackson।

Transport concerns belong near Handler DTOs।


JPA Annotations Are a Separate Trade-Off

Later we may pragmatically place JPA annotations on some domain classes if it keeps the project simpler without damaging the model।

That's a persistence trade-off।

But Jackson API annotations and JPA annotations should not automatically dictate domain semantics।


Current Classification

Based on current requirements:

Clear Entities

Product

Order

Stateful identity-bearing concept

Inventory

identified naturally by its Product association in v1।


Owned value-like domain object

OrderItem

No independent business identity required currently।


Potential Value Objects

ProductId

OrderId

CustomerId

if typed identifiers provide enough value to justify them।


DTOs

Examples:

CreateOrderRequest

CreateOrderResponse

ProductResponse

ProviderPaymentRequest

ProviderPaymentResponse

depending on boundary।


This Classification Can Evolve

Suppose future requirement says:

Admin can partially refund a specific Order Item.

Now independent Order Item identity may become much more valuable।

Or:

Order Item tracks fulfilment separately.

Then our model may evolve।

Domain classification follows business needs।

It is not permanent dogma।


Don't Future-Proof With Fake Identity

Knowing OrderItem might need an ID someday is not enough reason to create a rich identity model now।

Persistence can always add a technical row key if needed।

Domain identity should appear when business behaviour needs it।


Entity and Value Object Design Checklist

For a new domain concept ask:

Does identity matter?

Does state change while identity stays stable?

Could two equal-looking values still represent
different business things?

If identity does not matter,
is this concept defined entirely by its values?

Should it be immutable?

Does it protect repeated domain semantics?

DTO Design Checklist

For boundary data ask:

Which boundary is this transferring data across?

What fields does that boundary actually need?

Which fields are untrusted?

Are we exposing server-owned state unnecessarily?

Does this DTO accidentally couple API
to our internal domain shape?

Should provider-specific data remain
inside the external Service boundary?

Common Mistake 1 — Every Database Row Is a Domain Entity

A technical primary key does not automatically create meaningful domain identity।


Common Mistake 2 — JPA @Entity Defines the Domain

ORM annotations are persistence mechanics, not business semantics।


Common Mistake 3 — Domain Entity Used as Request Body

This gives external callers too much control and couples API to domain internals।


Common Mistake 4 — Domain Entity Returned Directly as API Contract

This can leak internal changes and fields into public responses।


Common Mistake 5 — Value Object for Every Primitive

Wrapping every String/Long/int creates noise if the type protects no meaningful semantics।


Common Mistake 6 — Mutable Value Objects

If identity does not matter, mutable value state usually makes reasoning harder।

Prefer immutable replacements।


Common Mistake 7 — DTO With Business Behaviour

Request/response classes should not become the place where Order lifecycle rules live।


Common Mistake 8 — Provider DTO Leaks Into UseCase

PayOrderUseCase should depend on application-facing payment results, not provider-specific response schemas।


Common Mistake 9 — Inventing OrderItem Identity

Current business does not require independently managed Order Items।

Don't create complexity merely because relational tables often use row IDs।


Common Mistake 10 — One Class for Domain, API, and Persistence Because It's Convenient

Sometimes reuse is pragmatic, but it should be a deliberate decision.

Don't let accidental convenience erase important boundaries।


How This Fits Our Application Architecture

Incoming flow:

HTTP
    ↓
DTO
    ↓
Handler
    ↓
UseCase

Inside the UseCase:

Entities
+
Value Objects

represent business concepts।

Persistence:

UseCase
    ↓
Repository

External integration:

UseCase
    ↓
Service
    ↓
Provider DTOs

Each type stays close to the responsibility it serves।


Example: Create Order

Incoming:

CreateOrderRequest
[DTO]

Handler obtains:

CustomerId
[Value Object candidate]

UseCase loads:

Product
[Entity]
Inventory
[stateful Entity-like concept]

Creates:

OrderItem
[owned value-like object]

Then:

Order
[Entity]

Handler maps result to:

OrderResponse
[DTO]

This gives us a clean model from transport to domain and back।


Engineering Principle

The core principle:

Entity identity survives state changes, Value Objects are defined by their values, and DTOs exist to carry data across boundaries.

Another:

Do not let database primary keys, JPA annotations, or JSON shapes decide what a business concept means.

And:

Use separate models where boundaries require different responsibilities, but do not multiply classes without a concrete reason.


Summary

In this lesson, we learned that:

  • Entities, Value Objects, and DTOs may all contain data but exist for different reasons.
  • Entity identity matters independently of current state.
  • Product and Order are clear domain Entities.
  • Inventory is stateful domain state naturally identified through Product in v1.
  • OrderItem currently has no independent business lifecycle or identity and can be modeled as an owned value-like object.
  • A database row ID does not automatically imply domain Entity identity.
  • JPA @Entity and domain Entity are different concepts.
  • Value Objects are defined by their values rather than independent identity.
  • Value Objects are usually immutable.
  • Typed IDs can improve type safety and domain expressiveness.
  • We should not wrap every primitive without a meaningful reason.
  • Java record is often useful for Value Objects but does not determine architectural role.
  • DTOs exist primarily to transfer data across boundaries.
  • Request/response DTOs belong near the Handler/API boundary.
  • External provider DTOs belong near the external Service boundary.
  • Domain Entities should not be used directly as untrusted request models.
  • Clients should not control Order status, ownership, authoritative prices, or totals.
  • Dedicated response DTOs help keep API contracts separate from domain internals.
  • Commands may represent UseCase input when that separation provides value.
  • We do not need Request → Command → Model layers mechanically for every operation.
  • Domain objects should maintain stronger invariants than untrusted DTOs.
  • Persistence model, domain model, and API model are conceptually separate even when some implementation types are pragmatically shared.
  • OrderItem immutability fits our requirement that quantity and purchase-time price do not change after successful Order creation.
  • Domain classification can evolve when future business requirements actually change.

Next lesson:

Separating Domain Logic from Transport Logic

There we will follow a request from HTTP → Handler → UseCase → Domain and establish exactly which validations and decisions belong at the HTTP boundary versus inside the application/domain model—without letting Spring MVC or JSON concerns leak into our business logic.