Domain Modeling

Turning Business Concepts into Domain Models

ReadingPreview

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

এখন পর্যন্ত আমরা system নিয়ে অনেক reasoning করেছি।

আমরা জানি আমাদের Order Management Backend-এর core business concepts:

Product
Inventory
Order
Order Item
Customer Identity
Payment

আমরা responsibilities-ও define করেছি।

For example:

Product
→ current product information
→ current price
→ active/inactive state
Inventory
→ available quantity
→ quantity cannot be negative
Order
→ belongs to a customer
→ contains Order Items
→ has a lifecycle
→ can be cancelled or paid only when valid

এখন প্রশ্ন:

এই business concepts-গুলো actual Java code-এ কীভাবে model করব?

এই lesson-এর goal কোনো JPA entity বানানো নয়।

Goal:

Business concepts, state, rules, এবং behaviour-কে Java model-এ এমনভাবে represent করা যাতে code domain-এর meaning preserve করে।


Domain Modeling Is Not Table Design

Backend development-এ একটি common mistake:

Requirement দেখেই database table design শুরু করা।

For example:

products
inventory
orders
order_items

তারপর Java classes বানানো:

ProductEntity
InventoryEntity
OrderEntity
OrderItemEntity

এবং মনে করা domain modeling complete।

But database structure only answers:

Data কীভাবে persist হবে?

Domain model answers a different question:

Business concept কী এবং কোন rules তার valid state protect করে?


Start From Business Language

Requirement says:

A customer can cancel an eligible unpaid order.

This sentence contains important domain meaning:

Customer
Order
Eligibility
Unpaid
Cancellation

If we model only:

order.setStatus("CANCELLED");

we have stored data, but we have not really modeled the rule।

A stronger model might expose:

order.cancel();

and let Order decide whether cancellation is valid।


The Domain Model Should Speak the Business Language

When reading code:

order.cancel();

an engineer understands the business operation immediately।

Compare:

order.setStatus(OrderStatus.CANCELLED);

This says only:

Change a field.

It does not communicate:

Is cancellation allowed?

What states can transition?

What rule protects the change?

Good domain modeling tries to make meaningful operations visible।


Data Model vs Domain Model

Consider:

public class Order {

    private Long id;
    private String status;
    private BigDecimal total;

    // getters and setters
}

This can store Order data।

But what business behaviour does it express?

Almost none।

Any caller could potentially do:

order.setStatus("BANANA");

or:

order.setStatus("PAID");
order.setStatus("CANCELLED");

without respecting business rules।

That is primarily a data structure, not a strong domain model।


A Better Direction

Conceptually:

public class Order {

    private OrderStatus status;

    public void cancel() {
        if (status != OrderStatus.UNPAID) {
            throw new IllegalStateException(
                    "Order cannot be cancelled"
            );
        }

        status = OrderStatus.CANCELLED;
    }

    public void markPaid() {
        if (status != OrderStatus.UNPAID) {
            throw new IllegalStateException(
                    "Order cannot be paid"
            );
        }

        status = OrderStatus.PAID;
    }
}

Now Order protects its own lifecycle।

The exact error types will improve later।

The important change is responsibility।


Domain Modeling Begins With Questions

For each business concept, ask:

What identifies it?

What state does it own?

What rules must always remain true?

What behaviour changes its state?

Which other concepts does it reference?

Which state belongs somewhere else?

These questions are more useful than asking:

Which annotations does this class need?


Let's Start With Product

From our requirements, Product needs:

stable identity

current product information

current price

active/inactive ordering state

A Product may be created, updated, or deactivated।

Inactive Product cannot be used for new Orders।

Product does not own:

Inventory quantity

historical Order Item price

Order lifecycle

This boundary matters।


A First Product Model

Conceptually:

public class Product {

    private final ProductId id;
    private String name;
    private BigDecimal price;
    private boolean active;

}

This already gives us some state।

But before adding getters and setters everywhere, ask:

What operations actually make sense?


Avoid Generic Mutation

Weak:

product.setPrice(newPrice);
product.setActive(false);

Better business-oriented operations may be:

product.changePrice(newPrice);
product.deactivate();

Why?

Because these methods can protect invariants।

Example:

public void changePrice(BigDecimal newPrice) {
    if (newPrice.signum() < 0) {
        throw new IllegalArgumentException(
                "Price cannot be negative"
        );
    }

    this.price = newPrice;
}

Now Product owns its own price validity rule।


Product Invariant

From design review:

Product price cannot be negative.

This is an invariant।

Meaning:

A valid Product object should never represent a negative price.

So this should not be enforced only in HTTP validation।

It should also be protected where Product state is created/changed।


Constructor Validation

Suppose:

public Product(
        ProductId id,
        String name,
        BigDecimal price
) {
    this.id = id;
    this.name = name;
    this.price = price;
    this.active = true;
}

If price can be negative, we can construct invalid Product state।

Better:

public Product(
        ProductId id,
        String name,
        BigDecimal price
) {
    if (price.signum() < 0) {
        throw new IllegalArgumentException(
                "Price cannot be negative"
        );
    }

    this.id = id;
    this.name = name;
    this.price = price;
    this.active = true;
}

Now invalid state is harder to create।


Why "Harder to Create Invalid State" Matters

If invalid state can exist freely:

invalid object
    ↓
moves through application
    ↓
fails much later

Debugging becomes harder।

Better:

invalid input
    ↓
rejected near construction/change

This keeps domain assumptions stronger।


Product Active State

We established:

active Product

and:

inactive Product

have different ordering semantics।

Instead of exposing:

product.setActive(false);

prefer:

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

Potentially:

public void activate() {
    this.active = true;
}

if reactivation is required by Product management behaviour।

The method name expresses intent।


Product Does Not Decide Inventory Availability

Important distinction:

Product active

does not mean:

Inventory available

A Product can be:

active
+
quantity = 0

and therefore not currently available to order।

So avoid:

product.isAvailable();

if that method only knows Product state but callers may interpret it as full order availability।

Better wording may be:

product.isActive();

or:

product.isOrderable();

only if we clearly define that this means Product state alone।

The combined "available for ordering" decision involves Product + Inventory।


Now Inventory

Inventory owns:

Product association

available quantity

Invariant:

quantity >= 0

Operations may include:

decrease quantity

increase quantity

set current quantity for admin adjustment

But operations should express rules।


Weak Inventory Model

public class Inventory {

    private int quantity;

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
}

This allows:

inventory.setQuantity(-100);

Invalid।

It also puts responsibility on every caller to remember the rule।


Better Inventory Behaviour

Conceptually:

public class Inventory {

    private final ProductId productId;
    private int quantity;

    public void decrease(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException(
                    "Amount must be positive"
            );
        }

        if (amount > quantity) {
            throw new IllegalStateException(
                    "Insufficient inventory"
            );
        }

        quantity -= amount;
    }

    public void increase(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException(
                    "Amount must be positive"
            );
        }

        quantity += amount;
    }
}

Now Inventory protects basic quantity rules।


What About Admin Adjustment?

Our v1 behaviour says:

Administrator sets current available quantity to a non-negative value.

So domain could expose:

public void setAvailableQuantity(
        int quantity
) {
    if (quantity < 0) {
        throw new IllegalArgumentException(
                "Quantity cannot be negative"
        );
    }

    this.quantity = quantity;
}

This is different from normal order consumption।

Both operations represent different intent:

decrease(...)
→ business consumption
setAvailableQuantity(...)
→ administrative adjustment

Meaningful method names help preserve that distinction।


Inventory Concurrency Is Not Solved Here

Important:

inventory.decrease(1);

can protect the in-memory object rule।

It does not automatically solve concurrent database requests।

Suppose two transactions load quantity 1 simultaneously।

Both objects may pass:

amount <= quantity

before persistence।

Concurrency-safe inventory consumption requires persistence/database strategy later।

So:

Domain invariants and database concurrency are related, but not the same problem.


Domain Model Should Not Pretend to Solve Infrastructure Problems

Inventory should know:

quantity cannot go below zero

It should not know:

SELECT FOR UPDATE

optimistic lock version

PostgreSQL transaction isolation

Those belong to persistence/application infrastructure।

This keeps boundaries clear।


Now Order

Order is more complex।

It owns:

stable identity

customer ownership

Order Items

lifecycle

total consistency

States for v1:

UNPAID

PAID

CANCELLED

Allowed transitions:

UNPAID → PAID

UNPAID → CANCELLED

Invalid:

PAID → CANCELLED

CANCELLED → PAID

This is strong domain behaviour।


Order Should Protect Its Lifecycle

Weak:

order.setStatus(OrderStatus.CANCELLED);

Any caller can bypass rules।

Better:

order.cancel();

and:

order.markPaid();

Order decides whether transition is valid।


Order State Transition Example

public void cancel() {
    if (status != OrderStatus.UNPAID) {
        throw new IllegalStateException(
                "Only unpaid orders can be cancelled"
        );
    }

    status = OrderStatus.CANCELLED;
}

Payment:

public void markPaid() {
    if (status != OrderStatus.UNPAID) {
        throw new IllegalStateException(
                "Only unpaid orders can be paid"
        );
    }

    status = OrderStatus.PAID;
}

Now state transitions are not arbitrary assignments।


Why Not Two Booleans?

Possible model:

boolean paid;
boolean cancelled;

Then states can become:

paid = true
cancelled = true

Is that valid?

No।

Now every caller must reason about combinations।

An explicit state:

OrderStatus.UNPAID
OrderStatus.PAID
OrderStatus.CANCELLED

makes valid lifecycle states clearer।


Model State That Matches the Business

Don't add:

PROCESSING

CONFIRMED

SHIPPED

DELIVERED

REFUNDED

because those sound realistic for commerce।

They are not current requirements।

Domain model should reflect current business scope, not imagined future lifecycle।


Order Item

An Order contains one or more Order Items।

Each Order Item carries:

Product reference

quantity

purchase-time unit price

Example:

public class OrderItem {

    private final ProductId productId;
    private final int quantity;
    private final BigDecimal unitPrice;

}

Important:

unitPrice

means:

Product price at the time this Order was created.

It is not current Product price।


Order Item Is Historical State

Suppose:

Order created
Product price = €20

Later:

Product price = €25

Order Item still holds:

€20

That is why OrderItem.unitPrice is not unnecessary duplication।

It represents a different fact।


Order Item Invariants

From requirements:

quantity > 0

unit price cannot be negative

So constructor can protect these rules।

Conceptually:

public OrderItem(
        ProductId productId,
        int quantity,
        BigDecimal unitPrice
) {
    if (quantity <= 0) {
        throw new IllegalArgumentException(
                "Quantity must be positive"
        );
    }

    if (unitPrice.signum() < 0) {
        throw new IllegalArgumentException(
                "Unit price cannot be negative"
        );
    }

    this.productId = productId;
    this.quantity = quantity;
    this.unitPrice = unitPrice;
}

Why Does Order Item Store ProductId?

Order references Product historically।

But Order does not own Product।

We established:

Order
    ↓ contains
Order Item
    ↓ references
Product

So a stable Product reference is enough for this relationship।

We do not embed the entire live Product object as owned Order state।


Composition

Order and Order Item have a strong lifecycle relationship।

Conceptually:

Order
    ├── OrderItem
    ├── OrderItem
    └── OrderItem

An Order Item does not exist as an independent user-facing resource in v1।

There is no:

Create Order Item API

separate from Order।

This is a natural composition relationship।


Order Should Own Its Items

Example:

public class Order {

    private final List<OrderItem> items;

}

Callers should not be able to freely mutate internal collection:

order.getItems().clear();

because then they could make an invalid zero-item Order।


Protect Collections

Instead of returning mutable internal list directly, expose an immutable/read-only view where practical।

Conceptually:

public List<OrderItem> items() {
    return List.copyOf(items);
}

Now caller cannot directly mutate Order internals through the returned list।

This is encapsulation applied to collections।


An Order Must Have at Least One Item

Invariant:

Order contains at least one Order Item.

Therefore constructor/factory should reject:

new Order(
        customerId,
        List.of()
);

Conceptually:

if (items.isEmpty()) {
    throw new IllegalArgumentException(
            "Order must contain at least one item"
    );
}

Again:

Invalid Order state should be hard to construct.


Duplicate Products

Requirement:

Same Product may appear at most once in one Order creation request.

Where should this be protected?

We previously decided the request/workflow should reject duplicate products before persistence।

Order construction can also protect the resulting invariant if duplicate Product Items should never exist in a valid Order।

Conceptually:

CreateOrderUseCase
    ↓ validates command
    ↓ creates valid Order Items
    ↓ constructs Order

The exact validation implementation comes later।

Important:

A valid Order should not casually represent duplicate lines for the same Product if the business rule forbids them.


Order Total

Our design review decided:

v1 Order total is derived from its Order Items.

Formula:

sum(
    unitPrice × quantity
)

So we do not need a freely mutable:

setTotal(...)

on Order।


Example Total Behaviour

Conceptually:

public BigDecimal total() {
    return items.stream()
            .map(item ->
                    item.unitPrice()
                            .multiply(
                                    BigDecimal.valueOf(
                                            item.quantity()
                                    )
                            )
            )
            .reduce(
                    BigDecimal.ZERO,
                    BigDecimal::add
            );
}

The exact implementation may change later।

The important point is ownership:

Order

owns total consistency because it owns Order Items।


Why Not Trust the Client Total?

Client may send:

{
  "total": 10
}

while server-side product prices produce:

€100

Client-controlled total cannot be authoritative।

Flow:

Product current price
        ↓
CreateOrderUseCase
        ↓
OrderItem unitPrice
        ↓
Order.total()

Backend determines the value։


Domain Model Does Not Load Product Prices

Should Order call:

productRepository.findById(...)

to calculate prices?

No।

That would make Order depend on persistence।

Instead:

CreateOrderUseCase

loads Product state and creates Order Items with trusted current prices।

Then Order works only with the data it owns।


Domain Model vs UseCase

This distinction is central।

UseCase coordinates

For Create Order:

identify customer

load Products

check Product state

load Inventory

check/decrease Inventory

capture prices

construct Order

persist state

Domain protects local state

Product:

valid price
active state

Inventory:

valid quantity

Order:

items
total
lifecycle

Order Item:

quantity
historical unit price

Neither responsibility should absorb the other।


Cross-Entity Rules Belong in the UseCase

Example:

Requested quantity must not exceed current Inventory.

Order alone cannot know current Inventory।

Product alone cannot know Order request।

So:

CreateOrderUseCase

coordinates that rule।


Local Entity Rules Belong in the Entity

Example:

Paid Order cannot be cancelled.

Order already owns:

status

So:

order.cancel();

is the natural place to enforce it।


A Useful Responsibility Test

Ask:

Can this rule be decided using only this object's owned state?

If yes, it may belong in the domain object।

Example:

Can Order be cancelled?

Order knows its status।

Good candidate।

If rule requires:

Product + Inventory + Customer + external provider

it likely belongs in a UseCase coordinating them।


Domain Objects Should Not Know Handlers

Never:

public class Order {

    private CreateOrderHandler handler;
}

Direction is wrong।

Architecture:

Handler
    ↓
UseCase
    ↓
Domain

Domain should not depend back upward।


Domain Objects Should Not Know Repositories

Avoid:

public class Order {

    private OrderRepository repository;

    public void save() {
        repository.save(this);
    }
}

This Active Record-style approach mixes domain state and persistence in ways we have not chosen for this architecture।

Our structure is:

UseCase
    ↓
Order
    ↓
OrderRepository called by UseCase

Domain Objects Should Not Know External Services

Avoid:

order.pay(paymentService);

if that causes Order to make an external provider call।

Order can know:

Can I transition to PAID?

But:

calling payment provider

belongs to PayOrderUseCase + PaymentService


Payment Flow

Correct conceptual separation:

PayOrderHandler
    ↓
PayOrderUseCase
    ↓
Order
    ↓ eligibility check

Then:

PayOrderUseCase
    ↓
PaymentService
    ↓
external provider

On confirmed success:

PayOrderUseCase
    ↓
order.markPaid()

Order protects transition।

UseCase coordinates the external operation।


Domain Model and Customer Identity

Do we need:

Customer

class now?

Our design says no local Customer aggregate is required for v1।

Order primarily needs:

stable customer identifier

Conceptually:

public class Order {

    private final CustomerId customerId;

}

This is enough to represent ownership।

We do not invent:

Customer password

email verification

profile preferences

inside Order domain।


Typed IDs

Instead of:

Long customerId
Long productId
Long orderId

we could eventually use:

CustomerId

ProductId

OrderId

Typed identifiers can prevent mistakes such as:

findOrder(productId);

when both are plain Long

But typed IDs also add types/code।

We have intentionally not mandated them yet।


Do Not Over-Model for Show

A common DDD-style overreaction:

OrderId
CustomerId
ProductId
Quantity
Money
OrderName
ProductName
OrderItemId
InventoryId

all introduced on day one।

Some may become useful।

But each abstraction has cost।

Rule:

Introduce a value object when it protects meaningful domain semantics or prevents repeated mistakes—not because every primitive must be wrapped.


Quantity

Could quantity simply be:

int

with validation?

Yes, for v1।

A dedicated:

Quantity

value type may be useful later if quantity rules become rich or repeated।

But we don't need it to prove we understand domain modeling।


Money

Price deserves special care।

Never use:

double

for exact monetary values।

Floating-point representation can introduce precision surprises।

For Java business applications, BigDecimal is commonly suitable for exact decimal monetary calculations।

So conceptually:

BigDecimal price

is a reasonable current direction।


BigDecimal Still Needs Discipline

BigDecimal does not automatically solve all money problems।

Questions still exist:

scale

rounding

currency

Our v1 does not include multi-currency or complex tax/discount calculations।

So don't create a complete Money framework yet।

Use exact decimal values and define additional rules when requirements require them।


Don't Add Currency If Scope Does Not Require It

If current system operates under one agreed currency context, introducing:

Money(
    BigDecimal amount,
    Currency currency
)

may be useful eventually, but it is not automatically required in this lesson।

We intentionally keep multi-currency out of scope।


Object Construction Matters

If a class has many setters:

Order order = new Order();

order.setCustomerId(...);
order.setStatus(...);
order.setItems(...);

there is a period where order exists but is incomplete।

Better construction can require essential state upfront।

Example:

Order order =
        new Order(
                orderId,
                customerId,
                items
        );

Now object starts closer to a valid state।


Not Every Field Belongs in the Constructor

If some state is generated later by persistence, construction strategy may need adjustment।

For example:

database-generated ID

may not exist before persistence।

We will resolve exact creation/persistence model later।

The principle:

Required business state should not be optional merely to make framework mapping easier.


Factory Methods

Sometimes named factory methods communicate intent better than overloaded constructors।

Example:

Order.create(
        customerId,
        items
);

could communicate:

create a new unpaid Order

while a persistence reconstruction path might differ।

Do we need this now?

Not necessarily।

We will choose constructor/factory patterns when implementation details become concrete।


Domain Reconstruction

Persistence later needs to load an existing Order with state such as:

PAID

This is different from creating a brand-new Order, which should initially be:

UNPAID

This distinction can influence constructor/factory design।

But we should not solve the JPA mapping mechanics before persistence module।

For now, understand:

new business creation

and:

reconstruct persisted state

are conceptually different operations।


Avoid Setters Just for JPA Before JPA Exists

Don't compromise the domain model now because:

JPA might need setters.

We have not yet designed JPA mappings।

Modern persistence approaches provide multiple mapping options।

First build a coherent domain model।

Then integrate persistence pragmatically।


Encapsulation

A core OOP principle from the previous course now becomes practical।

Encapsulation means an object protects its internal state and exposes meaningful behaviour।

Weak:

order.status = CANCELLED;

Strong:

order.cancel();

Weak:

inventory.quantity -= quantity;

Strong:

inventory.decrease(quantity);

Meaningful methods concentrate rules।


Encapsulation Does Not Mean "No Getters"

Read access can still be needed।

For example:

order.status()
order.total()
product.price()
inventory.quantity()

The problem is unrestricted mutation, not reading state itself।

Expose what callers need, but preserve ownership of state changes।


Avoid Getter/Setter Generation as Domain Design

IDE can generate:

getX
setX

for every field।

That is not domain modeling।

Before generating a setter ask:

Which business operation requires unrestricted replacement of this field?

Often the answer is none।


Example Product Behaviour

Instead of:

product.setPrice(newPrice);
product.setActive(false);

prefer:

product.changePrice(newPrice);
product.deactivate();

Now each operation can validate and communicate business intent।


Example Inventory Behaviour

Instead of:

inventory.setQuantity(
        inventory.getQuantity() - amount
);

prefer:

inventory.decrease(amount);

Now callers cannot accidentally bypass insufficient-inventory rules as easily।


Example Order Behaviour

Instead of:

order.setStatus(OrderStatus.PAID);

prefer:

order.markPaid();

The model reads like the domain।


Do We Need Inheritance?

Could we create:

BaseEntity

with:

id
createdAt
updatedAt

and make:

Product extends BaseEntity
Order extends BaseEntity
Inventory extends BaseEntity

Maybe technically possible।

But do these concepts share business behaviour because they are all one domain abstraction?

No।

This is persistence convenience, not meaningful inheritance।

We will not create BaseEntity merely to remove a few repeated fields।


Prefer Composition Over Artificial Inheritance

Order:

contains Order Items

This is a meaningful domain relationship।

Product and Inventory are associated by Product identity।

Use composition/reference where relationships are real।

Don't invent inheritance because classes share technical fields।


Domain Relationships Should Be Explicit

Our current conceptual model:

CustomerId
    ↓ owns
Order
    ↓ contains
OrderItem
    ↓ references
ProductId

ProductId
    ↓
Inventory

This is enough to reason about ownership।

It does not tell us exact JPA annotations.

That's intentional।


Domain Relationship Is Not JPA Relationship

Example:

Order Item references Product

does not automatically mean:

@ManyToOne
private Product product;

Maybe persistence will use that mapping।

Maybe it will store productId directly।

Domain meaning comes first।

Persistence representation comes later।


Don't Let ORM Decide the Domain

Weak approach:

What relationship annotation is easiest?

Then model domain around it।

Better:

What relationship does the business actually have?

Then choose persistence mapping that supports it।


Time in the Domain

Order history needs a way to order/understand when Orders were created।

So an Order creation timestamp is likely meaningful।

Conceptually:

Instant createdAt;

or another suitable Java time type।

But don't build a universal auditing framework yet।

We need:

Order creation time

because it has actual product/query meaning।

We do not automatically need:

createdBy
updatedBy
deletedBy
versionHistory

for every entity।


Use Java Time API

When timestamps become real, prefer modern Java time types such as:

Instant

LocalDate

OffsetDateTime

depending on semantics।

Avoid legacy:

java.util.Date

for new domain design unless integration constraints require it।

Exact timestamp semantics will be chosen when implemented।


Entity Identity

A Domain Entity has identity that matters across time।

Example:

Order #123

remains the same Order even if:

status changes

Similarly Product identity remains stable while:

price changes

This is one reason Product and Order are clear entities।


Inventory Identity Is More Subtle

Inventory may be identified through its Product relationship।

For v1:

one Inventory state per Product

may be enough।

We do not need to create a rich independent InventoryId unless persistence/domain needs it।

Again:

Model identity according to business meaning, not table conventions.


OrderItem Independent Identity?

Does a customer care about:

OrderItem #8272

independently of the Order?

Not in current scope।

Order Item belongs to Order lifecycle।

Database may later have an internal row ID, but that does not automatically make it a top-level domain entity with independent behaviour।


Persistence IDs and Domain IDs Can Differ

A table may need a primary key for technical reasons।

That does not necessarily mean the same identifier deserves exposure throughout the domain/API।

Keep technical persistence identity and business identity conceptually distinct।


Avoid Premature Domain Events

We could imagine:

OrderCreatedEvent

OrderCancelledEvent

InventoryChangedEvent

But current architecture has no event-driven requirement।

We do not need domain event infrastructure now।

If later Backend Engineering course introduces asynchronous workflows, the same domain can evolve।


Avoid Premature Aggregate Frameworks

We know Order + Order Items form a natural aggregate-like consistency boundary।

That does not mean we need:

AggregateRoot base class

DomainEventPublisher

AggregateRepository<T>

right now।

Use the concept to guide ownership and consistency, not to create framework ceremony।


Domain Model and Validation Layers

Suppose HTTP request quantity is:

-5

Handler-level validation may reject it before UseCase।

Should OrderItem still validate quantity?

Yes, if positive quantity is a domain invariant।

Different layers protect different boundaries।


Transport Validation

Handler may check:

required field exists

JSON shape valid

quantity has valid basic format

Domain Validation

Domain protects:

quantity > 0

price >= 0

valid Order state transition

because these must remain true regardless of whether object came from HTTP, a test, or another internal caller।


Persistence Constraint

Database may additionally enforce:

inventory quantity >= 0

where practical।

This protects stored state even if application bugs occur।

Multiple layers are not necessarily duplication when they protect different boundaries।


Don't Put Every Validation Everywhere

However, avoid blindly repeating every rule in:

Handler

UseCase

Domain

Repository

Database

Decide which boundary actually owns the rule।

Example:

JSON field required

doesn't belong in Order domain।

Example:

PAID Order cannot be cancelled

doesn't belong only in Handler।


Domain Exceptions

Our examples currently use:

IllegalArgumentException
IllegalStateException

for simplicity।

As application evolves, we may introduce domain-specific failures such as:

InvalidOrderState

InsufficientInventory

if they improve clarity and error handling।

We won't design an exception hierarchy prematurely।


Error Type Should Communicate Meaning

This:

IllegalStateException

is fine while demonstrating the rule।

But production application may need to distinguish:

business rejection

from:

programming bug

because Handler/API needs to map expected failures appropriately।

That design comes as workflows/API mature।


Keep Domain Framework-Light

Our domain model ideally remains understandable without Spring knowledge।

For example:

Order order = ...;

order.cancel();

should work in a plain JUnit test।

No Application Context needed।

This gives us:

fast tests

clear business behaviour

less framework coupling

Spring Around the Domain

Application composition:

Spring Beans
    ↓
Handler
    ↓
UseCase

During workflow:

UseCase
    ↓
Product
Inventory
Order
OrderItem

Spring manages application collaborators।

Domain objects remain normal Java objects।


Domain Modeling Is Iterative

We will not design the perfect final Order model today।

As we implement:

persistence

REST API

transactions

payment integration

we may learn that some domain model needs adjustment।

That's normal।

The goal is:

Start with a model that reflects current confirmed behaviour and evolve it deliberately.


Don't Optimize for Hypothetical Future Features

Order might eventually have:

shipping

returns

refunds

discounts

fulfilment

But not now।

If we add fields/states today for those features:

domain complexity

appears before business value।

Keep v1 model focused।


Our Initial Domain Direction

From current requirements, a reasonable conceptual model is:

Product
├── ProductId
├── name
├── price
└── active
Inventory
├── ProductId
└── availableQuantity
Order
├── OrderId
├── CustomerId
├── OrderStatus
├── createdAt
└── OrderItems
OrderItem
├── ProductId
├── quantity
└── purchaseTimeUnitPrice

Exact field types and persistence details will evolve।


Responsibilities

Product

Owns:

current product information

current price

active/inactive state

Protects:

price cannot be negative

Inventory

Owns:

available quantity

Protects:

quantity cannot be negative

Supports:

decrease

increase

administrative quantity adjustment

Order

Owns:

customer ownership reference

Order Items

Order lifecycle

total consistency

Protects:

at least one item

valid state transitions

Order Item

Owns:

Product reference

quantity

purchase-time price

Protects:

quantity positive

unit price non-negative

What the UseCase Still Owns

Even with good domain objects, CreateOrderUseCase remains responsible for coordination।

Conceptually:

CreateOrderUseCase

1. receive authenticated CustomerId
2. validate duplicate Product requests
3. load Products
4. confirm Product state
5. load Inventory
6. confirm/decrease quantities
7. capture current Product prices
8. create Order Items
9. create Order
10. persist changes

Domain modeling does not eliminate UseCases।

It makes the objects being coordinated stronger।


What the Handler Still Owns

CreateOrderHandler remains responsible for boundary concerns:

receive request

transport validation

obtain authenticated identity

map request to UseCase input

map result/error to response

Domain modeling does not move HTTP logic inward।


What Repository Still Owns

Repositories:

load state

save state

execute persistence-specific queries

They do not decide:

whether paid Order can be cancelled

because that is domain behaviour।


Example End-to-End Separation

Request:

POST /orders

Conceptually:

CreateOrderHandler
        ↓
CreateOrderUseCase
        ↓
ProductRepository
        ↓
Product

Then:

InventoryRepository
        ↓
Inventory.decrease(...)

Then:

new OrderItem(...)
        ↓
new Order(...)
        ↓
OrderRepository.save(...)

Each part has a clear role।


A Domain Modeling Checklist

When creating a domain class, ask:

What business concept does this represent?

What identifies it?

What state does it own?

Which state does it NOT own?

What invariants must always hold?

Which state changes have meaningful business names?

Can callers bypass those rules through setters?

Does this class know framework/infrastructure details?

Am I modeling a current requirement or an imagined future feature?

Can I test its important behaviour with plain Java?

Common Mistake 1 — JPA First

Starting with:

@Table
@Column
@ManyToOne

before understanding business ownership।

Persistence comes later।


Common Mistake 2 — Getter/Setter Model

A class with only fields + getters + setters may expose all state mutation without protecting rules।

Use meaningful behaviour where it exists।


Common Mistake 3 — Business Logic Only in UseCase

If UseCase manually checks and mutates every field:

if (order.getStatus() == UNPAID) {
    order.setStatus(CANCELLED);
}

Order becomes anemic।

Prefer:

order.cancel();

Common Mistake 4 — Everything in Domain Objects

The opposite mistake:

order.loadProduct();
order.decreaseInventory();
order.callPaymentProvider();
order.save();

Now domain entity coordinates infrastructure।

UseCase should handle cross-component workflows।


Common Mistake 5 — Future State Explosion

Don't add:

SHIPPED
REFUNDED
RETURNED

before requirements exist।


Common Mistake 6 — Artificial Base Classes

Avoid BaseEntity inheritance purely for repeated persistence fields।


Common Mistake 7 — Primitive Wrappers Everywhere

Not every int, Long, or String needs a value-object class immediately।

Add abstraction when it protects useful meaning।


Common Mistake 8 — Mutable Collections Exposed

Avoid giving callers direct mutation access to Order's internal items।


Common Mistake 9 — Domain Depends on Spring

Domain business behaviour should not require ApplicationContext, HTTP classes, or external clients।


Common Mistake 10 — Confusing Current and Historical State

Remember:

Product.price
→ current price
OrderItem.unitPrice
→ historical purchase-time price

They intentionally coexist।


How This Connects to Our Earlier Design

We are now turning this conceptual model:

Customer Identity
    ↓
Order
    ↓
Order Item
    ↓
Product
    ↓
Inventory

into Java objects while preserving ownership boundaries।

No architecture change has occurred।

We are implementing the design we already reviewed।


What We Are Not Doing Yet

Not yet:

JPA mappings

Flyway schema

REST DTO details

transaction annotations

inventory locking

payment persistence

Those will be introduced when their respective lessons require them।

This lesson is about the business model first।


Engineering Principle

The core principle:

A domain model should represent business meaning, valid state, and meaningful behaviour—not merely mirror database columns.

Another:

Put rules on the object that owns the state required to enforce them, and use UseCases to coordinate rules that span multiple concepts.

And:

Make invalid business state harder to create rather than relying on every caller to remember every rule.


Summary

In this lesson, we learned that:

  • Domain modeling is different from database table design.
  • Business language should drive the initial Java model.
  • Product, Inventory, Order, and Order Item have distinct ownership responsibilities.
  • Product owns current product state and price.
  • Inventory owns available quantity.
  • Order owns its lifecycle, items, customer reference, and total consistency.
  • Order Item owns quantity and historical purchase-time unit price.
  • Meaningful methods such as cancel(), markPaid(), decrease(), and deactivate() are preferable to unrestricted setters when business rules exist.
  • Domain objects should protect invariants at construction and mutation boundaries.
  • Product prices and Order Item historical prices represent different facts.
  • Order total should derive from the Order Items for v1.
  • Domain objects should not load repositories, read HTTP requests, call external Services, or know Spring configuration.
  • Cross-entity rules remain coordinated by UseCases.
  • Local entity rules belong on the object that owns the required state.
  • Domain objects generally remain plain Java objects rather than Spring Beans.
  • Domain relationships should not be designed from JPA annotations.
  • Database concurrency cannot be solved solely through in-memory domain validation.
  • Typed IDs and value objects can be useful, but should be introduced when they provide meaningful value.
  • BigDecimal is preferable to floating-point types for exact monetary values.
  • We should avoid artificial inheritance, premature domain events, speculative states, and unnecessary abstraction.
  • The goal is not a perfect final model; it is a model that accurately represents current confirmed business behaviour and can evolve deliberately.

Next lesson:

Entities, Value Objects, and DTOs

There we will distinguish three concepts that are often mixed together in Java backend applications: Domain Entities, Value Objects, and DTOs—and decide which of our Order Management concepts belong in each category.