Domain Modeling
Modeling Product
আপনি একটি free preview lesson দেখছেন।
আমাদের Order Management Backend-এর সবচেয়ে simple-looking domain conceptগুলোর একটি হলো:
Product
At first glance, Product বলতে মনে হতে পারে:
id
name
price
কিন্তু domain model হিসেবে Product-এর responsibility শুধু data ধরে রাখা নয়।
আমাদের current requirements অনুযায়ী Product:
has stable identity
has current product information
has current price
can be active or inactive
cannot have a negative price
এবং Product-এর current state influence করে:
whether it can participate in a new Order
এই lesson-এর goal:
Product-কে এমন একটি domain Entity হিসেবে model করা, যা নিজের state এবং local invariants protect করে, কিন্তু Inventory, HTTP, persistence, বা Order workflow-এর responsibility নেয় না।
Start From Confirmed Behaviour
Product নিয়ে আমরা already establish করেছি:
Admin can create Product.
Admin can update Product.
Admin can deactivate Product.
Inactive Product cannot be used for new Orders.
Existing Orders remain valid even if Product is later deactivated.
Product has a current price.
Product price cannot be negative.
এই rules-এর বাইরে আমরা এখন কিছু invent করব না।
For example, current scope-এ নেই:
product category
brand
SKU rules
discount
tax
images
weight
variants
shipping information
এসব realistic commerce features হলেও আমাদের current backend-এর requirement নয়।
Product Is an Entity
Product-এর stable identity আছে।
Suppose:
Product ID = P-100
Name = Mechanical Keyboard
Price = 100
Later:
Price = 90
Product change হয়েছে।
কিন্তু এটি still:
Product P-100
So Product is a domain Entity।
Its state can evolve while identity remains stable।
Initial Shape
Conceptually:
public class Product {
private final ProductId id;
private String name;
private BigDecimal price;
private boolean active;
}
This gives us:
identity
mutable product information
current price
active state
But fields alone are not enough।
We need to define:
how valid Product is created
how its state changes
which mutations are allowed
Product Identity
We could use:
long id
or:
String id
or a typed:
ProductId
For this course, a typed ID is a useful direction because Product identity appears across:
Product
Inventory
OrderItem
repositories
UseCases
So conceptually:
public record ProductId(
String value
) {
}
can make intent clearer।
Then:
ProductId productId
cannot be confused as easily with:
OrderId
CustomerId
Don't Over-Design ProductId Yet
We do not currently need to decide:
UUID
database sequence
prefixed ID
ULID
That belongs to persistence/ID strategy later।
So ProductId here represents the domain concept, not a final generation mechanism।
Product Name
Product needs human-readable information।
For now:
String name
is enough।
Should name be empty?
A Product without a usable name is unlikely to make sense।
So we can protect a basic invariant:
name must not be null or blank
Conceptually:
private static String requireName(
String name
) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException(
"Product name is required"
);
}
return name;
}
This is a domain validity rule, not merely HTTP validation।
Why Validate Inside Product Too?
Even if Handler later uses:
@NotBlank
on a request DTO, Product may also be created from:
tests
another internal workflow
data migration
future admin process
A valid Product should not depend on one HTTP Handler remembering to validate it।
Product Price
Current price is a core Product property।
A Product may change from:
100
to:
90
over time।
But Product price must never be:
negative
That is one of our accepted design rules।
Use BigDecimal
For exact monetary values, use:
BigDecimal
rather than:
double
For example:
private BigDecimal price;
We are intentionally not building a full Money abstraction yet because:
multi-currency is out of scope
tax calculation is out of scope
complex rounding rules are not required yet
Price Validation
Conceptually:
private static BigDecimal requireValidPrice(
BigDecimal price
) {
if (price == null) {
throw new IllegalArgumentException(
"Product price is required"
);
}
if (price.signum() < 0) {
throw new IllegalArgumentException(
"Product price cannot be negative"
);
}
return price;
}
Notice:
zero price
is allowed।
Why?
Because our confirmed rule is:
price cannot be negative
We did not decide:
price must be greater than zero
We should not silently invent that business rule।
Product Active State
A Product can be:
active
or:
inactive
Active means its Product state permits use in new ordering workflows।
Inactive means:
new Orders must not include it
But Product activation state is not the same as complete availability।
Product Active Does Not Mean Available Inventory
Suppose:
Product active = true
Inventory = 0
Customer still cannot successfully order it।
Therefore:
Product state
+
Inventory state
both matter।
We should not let Product pretend it owns Inventory availability।
Avoid Ambiguous isAvailable()
A method:
product.isAvailable()
could be misunderstood as:
Product active
AND
Inventory available
but Product knows only its own state।
Better:
product.isActive()
which communicates exactly what Product knows।
Then a browsing/order UseCase can combine:
product.isActive()
+
inventory.availableQuantity() > 0
when necessary।
Product Construction
A new Product should begin in a valid state।
Conceptually:
public Product(
ProductId id,
String name,
BigDecimal price
) {
this.id = Objects.requireNonNull(
id,
"Product ID is required"
);
this.name = requireName(name);
this.price = requireValidPrice(price);
this.active = true;
}
This establishes a clear creation rule:
new Product starts active
Is that confirmed?
Our admin flow allows Product creation and later deactivation, so treating newly created Product as active is a reasonable model if that is the intended create behaviour.
If Product creation later needs draft/inactive creation, we would change this deliberately rather than adding speculative states now।
An Alternative Explicit Factory
If we want creation intent to be more visible:
public static Product create(
ProductId id,
String name,
BigDecimal price
) {
return new Product(
id,
name,
price,
true
);
}
Then reconstruction of an existing persisted Product could later use another path।
But we do not need this extra ceremony yet unless persistence requirements make the distinction valuable।
Meaningful Mutation
Avoid:
setName(...)
setPrice(...)
setActive(...)
for every property।
Instead expose operations that communicate business intent।
For example:
product.rename(...);
product.changePrice(...);
product.deactivate();
Potentially:
product.activate();
if admin requirements support reactivation।
Why changePrice() Is Better Than setPrice()
Compare:
product.setPrice(newPrice);
with:
product.changePrice(newPrice);
The second communicates:
this is a Product business operation
and naturally provides a place to enforce:
price validation
Example:
public void changePrice(
BigDecimal newPrice
) {
this.price = requireValidPrice(
newPrice
);
}
Product Rename
If Product information can be updated, Product name may change।
Conceptually:
public void rename(
String newName
) {
this.name = requireName(
newName
);
}
Again, mutation passes through the same invariant used during construction।
Avoid Duplicated Validation Logic
Bad:
public Product(...) {
if (...) {
...
}
}
public void rename(...) {
if (...) {
...
}
}
with slightly different rules।
Centralize small invariant checks in private helpers where appropriate।
For example:
private static String requireName(
String name
) {
...
}
This is internal domain implementation, not a generic global utility।
Product Deactivation
Conceptually:
public void deactivate() {
this.active = false;
}
Simple।
Should calling it twice fail?
Current requirements do not say that repeated deactivation is an error।
We could make it idempotent:
active → inactive
inactive → inactive
This is usually simple and practical।
No need to throw merely because Product is already inactive unless business behaviour requires it।
Reactivation
Admin requirements originally include:
create
view
update
deactivate
They do not explicitly require reactivation।
Therefore we should be cautious about adding:
activate();
just because a boolean supports it।
If update semantics later explicitly allow reactivation, we add it then।
For now:
deactivation
is confirmed behaviour।
Why Not Use ProductStatus?
We could model:
enum ProductStatus {
ACTIVE,
INACTIVE
}
instead of:
boolean active
Would that help?
With only two states and no additional lifecycle complexity, boolean can remain sufficient।
An enum becomes more valuable if future states appear such as:
DRAFT
ARCHIVED
but those do not exist today।
Don't add future state machinery prematurely।
Boolean Naming Matters
If using a boolean, prefer:
private boolean active;
and:
public boolean isActive() {
return active;
}
rather than confusing names like:
enabled
available
visible
published
unless those concepts actually mean something different।
Use domain language consistently।
Read Access
Product consumers may need:
id()
name()
price()
isActive()
Conceptually:
public ProductId id() {
return id;
}
public String name() {
return name;
}
public BigDecimal price() {
return price;
}
public boolean isActive() {
return active;
}
Read access is fine।
The important restriction is:
state-changing behaviour remains controlled
A Concrete Product Model
Putting the current ideas together:
package io.liveklass.ordermanagement.product.domain;
import java.math.BigDecimal;
import java.util.Objects;
public class Product {
private final ProductId id;
private String name;
private BigDecimal price;
private boolean active;
public Product(
ProductId id,
String name,
BigDecimal price
) {
this.id = Objects.requireNonNull(
id,
"Product ID is required"
);
this.name = requireName(name);
this.price = requireValidPrice(price);
this.active = true;
}
public ProductId id() {
return id;
}
public String name() {
return name;
}
public BigDecimal price() {
return price;
}
public boolean isActive() {
return active;
}
public void rename(
String newName
) {
this.name = requireName(
newName
);
}
public void changePrice(
BigDecimal newPrice
) {
this.price = requireValidPrice(
newPrice
);
}
public void deactivate() {
this.active = false;
}
private static String requireName(
String name
) {
if (
name == null ||
name.isBlank()
) {
throw new IllegalArgumentException(
"Product name is required"
);
}
return name;
}
private static BigDecimal requireValidPrice(
BigDecimal price
) {
if (price == null) {
throw new IllegalArgumentException(
"Product price is required"
);
}
if (price.signum() < 0) {
throw new IllegalArgumentException(
"Product price cannot be negative"
);
}
return price;
}
}
This is already a meaningful domain Entity।
No Spring dependency।
No JPA dependency।
No HTTP dependency।
ProductId
A minimal conceptual value object:
package io.liveklass.ordermanagement.product.domain;
import java.util.Objects;
public record ProductId(
String value
) {
public ProductId {
Objects.requireNonNull(
value,
"Product ID is required"
);
}
}
Would we also reject blank values?
Possibly, if our final ID representation is string-based and blank is never valid।
But because ID generation strategy is not finalized, don't build assumptions around formatting yet।
The important concept is typed identity।
Is ProductId Necessary Right Now?
We could alternatively use:
Long
until persistence chooses IDs।
That would be simpler।
Both approaches are reasonable।
For the course, typed IDs are useful to demonstrate domain semantics, but we should still avoid pretending the underlying ID strategy is already decided।
Product Does Not Own Inventory
Do not write:
public class Product {
private int inventoryQuantity;
}
because Product and Inventory have separate responsibilities in our accepted design।
Why separate them?
Product changes when:
name changes
price changes
activation state changes
Inventory changes when:
customer orders
order is cancelled
admin adjusts quantity
These have different workflows and consistency concerns।
Why Product and Inventory Should Remain Separate
Suppose Product contains:
price
availableQuantity
Then every Product update risks mixing:
catalog update
with:
inventory concurrency
Later an admin changing Product name should not accidentally participate in Inventory mutation semantics।
Separate model keeps responsibilities clearer।
Product Does Not Decide Combined Orderability
Don't put:
public boolean canBeOrdered(
int requestedQuantity
) {
}
inside Product if that requires Inventory state।
Product does not own available quantity।
Instead:
CreateOrderUseCase
coordinates:
Product active?
Inventory sufficient?
Product Does Own Its Own Eligibility State
However, Product can expose:
public boolean isActive()
or potentially a domain method that rejects inactive use if there is a meaningful local operation।
For now, simple state inspection is enough because the workflow needs Product + Inventory together।
Create Order Interaction
Conceptually:
CreateOrderUseCase
↓
ProductRepository
↓
Product
Then:
if (!product.isActive()) {
// application-level rejection
}
Then:
InventoryRepository
↓
Inventory
and availability is coordinated।
Product does not load Inventory itself।
Product Browse Interaction
Customer browse requirement says:
only currently orderable Products
Our design review clarified this means:
Product state permits ordering
AND
Inventory quantity > 0
So BrowseProductsUseCase may combine:
Product
Inventory
or repository/query projection may do this efficiently later।
But Product domain itself should not pretend to know Inventory।
Product Deactivation and Existing Orders
Suppose:
Order created while Product active
Then later:
Product deactivated
Should unpaid Order become invalid?
Our accepted design says:
No.
Existing eligible unpaid Order can still be paid।
Why?
Because Order already captured its own historical state:
ProductId
quantity
purchase-time unit price
Existing Order lifecycle should not depend on Product's current active state।
This Shows Why Boundaries Matter
If Order held a live Product reference and constantly asked:
product.isActive()
before payment, Product deactivation could accidentally invalidate existing Order behaviour।
But our domain model distinguishes:
Product current state
from:
Order historical state
This separation is deliberate।
Product Price and Existing Orders
Same reasoning:
Product price today:
100
Order created:
OrderItem.unitPrice = 100
Later Product price changes:
120
Existing Order still uses:
100
Product owns current price only।
It does not retroactively update Order Items।
Product Has No Order Collection
Avoid:
public class Product {
private List<Order> orders;
}
Product does not own Orders।
This would create unnecessary coupling and potentially huge object graphs।
Order Items reference Product identity where needed।
Product Has No Inventory Object Reference by Default
Avoid automatically:
private Inventory inventory;
inside Product merely because one Inventory exists per Product।
The domain relationship is:
Inventory belongs to Product identity
but that does not require Product to hold an Inventory object directly।
UseCase can coordinate both concepts।
Domain Relationship vs Object Graph
This is important.
Business relationship:
Inventory exists for Product
does not automatically mean Java must be:
product.getInventory()
Likewise persistence relation does not automatically mean:
@OneToOne
We will decide object/persistence mapping separately।
Product Persistence Is Not Part of This Class Yet
We do not add:
product.save();
or:
ProductRepository repository
inside Product।
Persistence flow remains:
UseCase
↓
Product
↓
ProductRepository
For example:
CreateProductUseCase
constructs Product and calls repository।
CreateProductUseCase
Conceptually:
public class CreateProductUseCase {
private final ProductRepository productRepository;
public CreateProductUseCase(
ProductRepository productRepository
) {
this.productRepository =
productRepository;
}
public Product execute(
CreateProductCommand command
) {
Product product =
new Product(
command.productId(),
command.name(),
command.price()
);
return productRepository.save(
product
);
}
}
Exact ID creation may later move elsewhere depending on persistence strategy।
The important point:
UseCase coordinates
Product validates Product state
Repository persists
Product Update UseCase
Conceptually:
UpdateProductUseCase
↓
load Product
↓
product.rename(...)
↓
product.changePrice(...)
↓
save
The UseCase decides which requested updates should happen।
Product protects the validity of each state change।
Product Deactivation UseCase
Conceptually:
DeactivateProductHandler
↓
DeactivateProductUseCase
↓
ProductRepository
↓
Product.deactivate()
↓
ProductRepository
No raw database update such as:
UPDATE products
SET active = false
directly from Handler।
The domain operation remains visible।
But Do We Always Need to Load the Entity?
For a simple update, repository could potentially execute an efficient database operation directly।
However, if Product domain behaviour/invariants are important, loading the Product makes the rules explicit।
We will balance domain integrity and persistence efficiency when concrete repository implementation arrives।
Don't optimize prematurely।
Product Validation and HTTP Validation
Request DTO might later contain:
public record CreateProductRequest(
String name,
BigDecimal price
) {
}
Handler can reject:
missing name
missing price
Product still protects:
name valid
price non-negative
because those are business-state invariants।
Product Error Semantics
Current examples throw:
IllegalArgumentException
for invalid construction/mutation।
This is acceptable while building the model।
Later we may introduce meaningful errors such as:
InvalidProductPrice
InvalidProductName
if API/business error handling benefits from them।
Do not create a large exception hierarchy before we need it।
Product Equality
Because Product is an Entity, conceptual equality should be identity-based।
Two Products:
ProductId P-100
represent the same Product identity even if their mutable state differs due to loading/version timing।
We should be careful when implementing:
equals()
hashCode()
especially once JPA enters the picture।
Entity equality with ORM proxies/generated IDs has practical trade-offs।
We will avoid prematurely implementing custom equality until the persistence model is clear।
Don't Generate equals() on All Fields
An IDE-generated:
equals(name, price, active)
would be wrong conceptually for Product identity।
Why?
Changing Product price would change equality semantics।
Entity identity should not depend on mutable fields।
This is another reason to be deliberate with generated methods।
Don't Use Lombok @Data
A common pattern:
@Data
public class Product {
}
would automatically generate:
getters
setters
equals
hashCode
toString
But this can undermine domain design by:
exposing unrestricted setters
creating field-based equality
hiding intentional API
We intentionally do not need Lombok for this model।
toString() and Sensitive Data
Product currently contains no obvious secrets।
Still, automatically dumping entire entity state into logs is not always desirable।
As domain models grow, be deliberate about what is logged rather than relying on auto-generated full-object output everywhere।
Product Mutability Is Intentional
Product is not immutable because its business state changes:
name changes
price changes
active state changes
But mutation is controlled through specific methods।
This is different from:
everything mutable
Good Entity design often means:
stable identity
controlled mutable state
Product Fields and Ownership
Let's classify:
id
stable identity
Product owns it।
name
current product information
Product owns it।
price
current Product price
Product owns it।
active
current Product lifecycle state
Product owns it।
availableQuantity
Inventory responsibility
Not Product।
purchaseTimePrice
OrderItem responsibility
Not Product।
customerId
Order responsibility
Not Product।
This is the kind of ownership reasoning that keeps models clean।
Product Does Not Know Admin Role
Should Product do:
product.changePrice(
currentUser,
newPrice
);
and verify:
currentUser is ADMIN
No।
Authorization requires runtime security context and belongs to the UseCase/application boundary।
Flow:
Handler/Security
↓
Admin identity/permission
↓
UpdateProductUseCase
↓
Product.changePrice(...)
Product only knows whether price itself is valid।
Product Does Not Know HTTP
No:
@ResponseStatus
No:
@JsonProperty
merely to define Product API contract।
No:
HttpStatus
inside Product domain behaviour।
The Handler/DTO layer owns transport concerns।
Product Does Not Know Spring
No:
@Component
on Product।
Product is created per business operation or reconstructed from persistence।
It is not a reusable Spring application component।
Product Domain Tests
This model should be testable with plain Java/JUnit।
Examples of meaningful tests:
Product can be created with zero price.
Product cannot be created with negative price.
Product price can be changed.
Product price cannot change to negative.
Product can be deactivated.
Product name cannot be blank.
No Spring context required।
Example Test: Negative Price
Conceptually:
@Test
void productCannotHaveNegativePrice() {
assertThrows(
IllegalArgumentException.class,
() -> new Product(
productId(),
"Keyboard",
new BigDecimal("-1.00")
)
);
}
This test protects a domain invariant।
Example Test: Change Price
@Test
void changesProductPrice() {
Product product =
new Product(
productId(),
"Keyboard",
new BigDecimal("100.00")
);
product.changePrice(
new BigDecimal("90.00")
);
assertEquals(
new BigDecimal("90.00"),
product.price()
);
}
BigDecimal Equality Caveat
Java's:
BigDecimal.equals()
considers scale।
So:
new BigDecimal("90.0")
and:
new BigDecimal("90.00")
are numerically equal via:
compareTo()
but not necessarily via:
equals()
depending on scale।
This becomes relevant in monetary tests and persistence।
We should use deliberate comparison or normalized scale strategy when money implementation is finalized।
Do We Normalize Price Scale Now?
Not yet।
We have not defined:
currency
required decimal scale
rounding strategy
So don't invent:
always force scale 2
without a confirmed monetary model।
For now, protect non-negativity and use exact decimal arithmetic।
Product Creation State
Our conceptual constructor makes Product active by default।
This means external caller cannot do:
new Product(
id,
name,
price,
false
);
for normal creation।
That can be a useful invariant if Product creation always means active catalog Product।
But persistence reconstruction later must restore existing inactive Product state।
Creation vs Reconstruction
This exposes an important design issue.
New Product creation:
starts active
Persisted Product reconstruction:
must restore active or inactive state
We may later need a separate persistence construction path।
For example:
Product.create(...)
versus:
Product.restore(...)
or a persistence constructor。
We do not need to finalize the mechanism until JPA integration।
But we should recognize the distinction।
Don't Add Public "Restore Anything" APIs Carelessly
A public method such as:
Product.restore(
id,
name,
price,
active
);
can bypass normal business creation rules if used incorrectly।
Persistence reconstruction is an infrastructure need।
We will design it carefully when persistence arrives rather than exposing unrestricted state restoration prematurely।
Product and Soft Delete
Our accepted behaviour is:
deactivate Product
not:
physically delete Product
Why?
Historical Order Items reference Product identity।
Deleting Product records may make historical relationships harder to preserve।
So current domain behaviour:
deactivate()
fits the business requirement better than:
delete()
inside Product।
"Delete" Is Not a Domain Method
We should not create:
product.delete();
because object deletion is persistence lifecycle, and business requirement is specifically deactivation।
Use business language:
deactivate
not database language:
delete
Product History
Current requirements do not say we need to preserve every historical Product name/price change।
We only preserve purchase-time price in Order Items।
So Product itself can represent current state:
current name
current price
active flag
No Product version-history model needed।
Don't Add Audit History Yet
Avoid:
ProductPriceHistory
ProductChangeLog
ProductVersion
until requirement exists।
Operational auditing may appear later, but not as speculative Product domain state now।
Product and Inventory Availability
Let's reinforce the accepted definition:
Customer-visible orderability requires:
Product active
AND
Inventory quantity > 0
Product domain only answers:
Am I active?
Inventory answers:
How much is available?
UseCase/query combines them।
That separation prevents Product from becoming a grab-bag of commerce state।
Product and Order Creation
A simplified flow:
CreateOrderUseCase
↓
ProductRepository.find(...)
↓
Product
Then:
Product inactive?
↓
reject Order creation
Otherwise:
capture Product.price()
Then create:
OrderItem
with:
purchase-time unit price
Product current state is read; Order Item owns the historical snapshot।
Product Price Is Not a Quote
Our design review established another important rule:
Product price observed during earlier browsing is not guaranteed as a locked quote.
At successful Order creation:
backend reads current Product price
and captures it。
So Product does not need:
price reservation
quote expiry
price lock
for v1।
This Keeps Product Focused
Product responsibilities remain small:
identity
name
current price
active state
local invariants
That's good।
A strong domain model does not need to be large।
It needs to be accurate।
Potential Final Package
When this capability is implemented:
product/
└── domain/
├── Product.java
└── ProductId.java
Later:
product/
├── handler/
├── usecase/
├── domain/
└── repository/
will grow as Product tickets are implemented।
No need to create empty folders ahead of time।
Review the Product API Surface
A healthy Product class might expose roughly:
id()
name()
price()
isActive()
rename(...)
changePrice(...)
deactivate()
This is intentionally small।
Compare to a generated model with:
setId
setName
setPrice
setActive
setInventory
setCreatedAt
setUpdatedAt
The smaller API communicates ownership more clearly।
Ask Before Adding a Product Method
If someone proposes:
product.reserve(quantity);
ask:
Does Product own Inventory?
No।
If someone proposes:
product.pay();
ask:
Does Product own Order/payment lifecycle?
No।
If someone proposes:
product.save();
ask:
Does Product own persistence?
No।
Product methods should stay within Product responsibility।
Common Mistake 1 — Product as JPA Table First
Starting with:
@Entity
@Getter
@Setter
and calling it domain modeling।
We start from business responsibility instead।
Common Mistake 2 — Inventory Inside Product
This mixes catalog state with Inventory state and concurrency concerns।
Common Mistake 3 — Public Setters
They allow callers to bypass Product invariants and intent-specific behaviour।
Common Mistake 4 — Product Checks Admin Role
Authorization belongs to Handler/UseCase/security boundary।
Common Mistake 5 — Product Knows Orders
Product should not maintain an Order collection or mutate Order lifecycle।
Common Mistake 6 — Product Current Price Updates Historical Orders
Order Items preserve purchase-time price independently।
Common Mistake 7 — isAvailable() With Ambiguous Meaning
Product alone does not know Inventory availability।
Use precise naming।
Common Mistake 8 — Add Reactivation Without Requirement
Do not infer full lifecycle just because active is a boolean।
Common Mistake 9 — Zero Price Rejected Without Requirement
Current rule only prohibits negative price।
Do not invent a minimum price rule।
Common Mistake 10 — Full Money Framework Too Early
BigDecimal is sufficient for current price requirements while broader currency/rounding concerns remain out of scope।
Product Domain Checklist
Before considering Product model ready, ask:
Does Product have stable identity?
Can invalid negative price be created?
Can callers change price without validation?
Can callers arbitrarily mutate active state?
Does Product accidentally own Inventory?
Does Product know HTTP or Spring?
Does Product know persistence?
Can existing Order history remain independent
from Product price changes?
Are we modeling only confirmed Product behaviour?
Current Product Model
Our conceptual Product is:
Product
├── ProductId
├── name
├── current price
└── active state
Behaviour:
rename
changePrice
deactivate
Invariant:
name is meaningful
price is not negative
Not owned by Product:
Inventory quantity
Order history
purchase-time price
customer authorization
payment
persistence mechanics
This is a focused domain Entity।
Engineering Principle
The core principle:
A Product represents current catalog state and should protect its own invariants without absorbing Inventory, Order, security, or persistence responsibilities.
Another:
Expose business operations such as
changePrice()anddeactivate()instead of generic mutation when the state change has domain meaning.
And:
Current Product state affects new workflows; historical Orders must remain independent from future Product changes.
Summary
In this lesson, we learned that:
- Product is a domain Entity because its identity remains stable while its state changes.
- Product owns current name, current price, and active/inactive state.
- Product price cannot be negative.
- Zero price remains valid unless a future business rule says otherwise.
BigDecimalis suitable for current exact decimal price modeling.- Product should be created in a valid state.
- Meaningful methods such as
rename(),changePrice(), anddeactivate()are preferable to unrestricted setters. - Product does not own Inventory quantity.
- Product activation and Inventory availability are separate concepts.
- Customer-visible orderability requires Product state and Inventory state to be considered together.
- Product should not expose an ambiguous
isAvailable()if it cannot represent Inventory availability. - Product should not know Handler, UseCase, Repository, Spring Security, HTTP, or external Service details.
- Product current price does not overwrite historical Order Item prices.
- Product deactivation does not invalidate existing eligible Orders.
- Product does not require Order collections or direct Inventory object ownership.
- Typed
ProductIdcan improve domain clarity, but the final ID generation strategy remains deferred. - Entity equality should be identity-oriented, so we should not generate field-based equality blindly.
- Product should not use generic Lombok-style setters/equality generation by default.
- New Product creation and persistence reconstruction are conceptually different and may require separate construction paths later.
- Physical deletion is not current business behaviour; deactivation is.
- We should not add Product history, reactivation, quote locking, categories, variants, or other future features without requirements.
- A good Product model can remain small while still being a strong domain model.
Next lesson:
Modeling Customer
There we will model the customer identity used by Orders without creating a local Customer account aggregate, because authentication and identity lifecycle belong to the external identity system while our backend only needs a stable authenticated customer reference.