Domain Modeling

Modeling Product

ReadingPreview

আপনি একটি 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() and deactivate() 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.
  • BigDecimal is suitable for current exact decimal price modeling.
  • Product should be created in a valid state.
  • Meaningful methods such as rename(), changePrice(), and deactivate() 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 ProductId can 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.