Domain Modeling
Entities, Value Objects, and DTOs
আপনি একটি 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:
recorddoes 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
@Entityand 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
recordis 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.