Designing the System
Identifying Domain Entities
In the previous lesson, we started moving from requirements into technical design.
We established that system design should begin with business behaviour:
Business Behaviour
↓
Responsibilities
↓
Domain Concepts
↓
Technical Design
Now we need to identify the important Domain concepts inside our Order Management Backend.
Our requirements repeatedly refer to:
Customer
Product
Inventory
Order
OrderItem
Payment
But finding nouns in a requirement and creating one Java class for each noun is not Domain modelling.
We need to understand:
- which concepts have meaningful identity
- which concepts have their own lifecycle
- which concepts belong inside another concept
- which values represent current state
- which values represent historical facts
- which business rules each concept should protect
- which concepts are independent Entities
- which concepts are owned by another Entity
In this lesson, we will stay at the Domain-design level.
We will not design JPA mappings or final database tables yet.
The goal is to build a clear model of the business problem.
What Is a Domain?
In software engineering, the domain is the problem space the software exists to solve.
Our broad business area is commerce.
But we are not building an entire commerce platform.
Our current problem space includes:
Product catalog
Inventory
Ordering
Order history
Cancellation
Payment
Outside our current scope:
Shipping
Warehouses
Discounts
Coupons
Returns
Refunds
Recommendations
Supplier management
Accounting
This boundary matters.
A concept may exist in a real commerce platform without belonging in our current application.
The Domain model should represent the problem we are solving now.
What Is a Domain Model?
A Domain model represents business:
Identity
+
State
+
Relationships
+
Rules
+
Behaviour
Consider Order.
It is not merely:
id
customerId
status
createdAt
It also has meaningful behaviour:
Can this Order be paid?
Can this Order be cancelled?
Which Customer owns it?
Which OrderItems belong to it?
Which state transitions are valid?
What historical pricing does it preserve?
A useful Domain model captures these meanings instead of treating everything as mutable data.
What Is an Entity?
An Entity is a Domain concept whose identity matters.
Suppose we have:
Product A
Name: Keyboard
Price: 5000 cents
and another:
Product B
Name: Keyboard
Price: 5000 cents
Even if all visible properties are the same:
Product A != Product B
because they have different identities.
An Entity's properties may change while its identity remains stable.
For example:
Product A
priceCents = 5000
↓
priceCents = 5500
It is still the same Product.
Domain Entity Is Not JPA @Entity
This distinction is important.
In Domain modelling:
Entity
means:
a business concept
whose identity and lifecycle matter
In JPA:
@Entity
is a persistence mechanism.
When we say:
Orderis a Domain Entity.
we are not yet deciding that the exact same Java class must carry:
@Entity
The persistence model comes later.
Similarly:
Domain Entity
≠
Database Table
They may map closely later, but they are different design concepts.
Value-Like Concepts
Not every concept needs independent identity.
For example:
2000 cents
and another:
2000 cents
represent the same monetary value in the same context.
We do not need:
Money #123
Money #456
to distinguish them.
Other value-like concepts include:
Quantity
OrderStatus
Price
But this does not mean every value needs a custom Java class.
A Domain concept and an implementation abstraction are separate decisions.
For example, our current money representation is simply:
long priceCents;
We do not create a Money value object merely for ceremony.
Start From Business Language
Our requirements say:
Customers place Orders.
Orders contain Products.
Products have associated Inventory.
Orders preserve purchase-time prices.
Customers can cancel eligible Orders.
Customers can pay for Orders.
These statements give us clues about the Domain.
Now let's analyze each concept.
Customer Identity
A Customer:
- creates Orders
- views their own Orders
- cancels their own eligible Orders
- pays for their own Orders
Identity matters because:
Customer A
and:
Customer B
are different actors.
The critical ownership rule is:
Customer A cannot access
Customer B's Orders.
So the ordering application needs a stable Customer identity.
Do We Need a Customer Entity?
No.
Our engineering context already tells us that authentication comes from an existing identity capability.
Our Order Management Backend does not own:
Registration
Password storage
Password reset
Email verification
MFA
Account lifecycle
The ordering domain only needs:
CustomerId
so it can express:
This Order belongs to Customer X.
Conceptually:
Identity System
↓
authenticated identity
↓
CustomerId
↓
Order Management Backend
The external identity system owns authentication.
Our backend owns Order ownership.
CustomerId
For this project, CustomerId is a small Domain/Application concept.
It represents a stable identifier supplied by the authentication context.
We deliberately do not assume that it is a UUID.
Conceptually:
public record CustomerId(
String value
) {
}
The exact implementation comes later.
The important rule is:
CustomerId is stable and non-blank.
There is no local Customer table in our current design.
Product
Product is clearly a Domain Entity.
A Product has:
- stable identity
- current catalog information
- current price
- active/inactive state
- a lifecycle independent from Orders
Conceptually:
ProductId
name
priceCents
active
For example:
Product P1
priceCents = 2000
active = true
Later:
Product P1
priceCents = 2500
active = true
Still the same Product.
Later:
Product P1
active = false
Still the same Product.
Therefore:
Product is a Domain Entity.
Product Identity
Our project uses UUID-based Product identity.
Conceptually:
ProductId
wraps
java.util.UUID
The application generates Product IDs.
For example:
ProductId.newId();
The HTTP API will expose the UUID in textual form.
The business identity belongs to the Product itself.
Product Responsibilities
For the current v1 scope, Product owns:
stable identity
current name
current priceCents
active/inactive state
It can also protect Product-local rules such as:
name must be valid
priceCents cannot be negative
and meaningful transitions such as:
deactivate
But there is an important distinction:
An active Product is not automatically orderable.
For example:
Product.active = true
Inventory.availableQuantity = 0
The Product is active from the catalog perspective.
But a Customer cannot currently order it.
So:
Product state
and:
Inventory availability
are different facts.
Inventory
We could have modeled Inventory directly inside Product:
Product
├── name
├── priceCents
├── active
└── availableQuantity
But our requirements suggest that Inventory is a separate concern.
Why?
Product changes for reasons such as:
name changes
price changes
deactivation
Inventory changes for different reasons:
Administrator sets stock
Order creation consumes stock
Order cancellation restores stock
These mutation patterns and responsibilities are different.
Therefore:
Inventory is a separate stateful Domain concept.
Inventory Identity
Inventory does not need its own independent InventoryId.
For v1:
Inventory identity
=
ProductId
Conceptually:
Inventory for Product P1
is sufficient.
This means one Product can have at most one Inventory state in the current model.
Later persistence may use:
product_id
as the Inventory primary key.
This avoids inventing an identifier with no business value.
Inventory Responsibilities
Inventory owns:
ProductId
availableQuantity
and Inventory-specific rules such as:
availableQuantity >= 0
It may expose meaningful behaviour such as:
set available quantity
decrease quantity
increase quantity
But concurrency-safe Order consumption will require persistence support beyond an in-memory Domain method.
That distinction becomes important later.
Inventory Invariants
The basic invariant is:
availableQuantity >= 0
For Order creation:
requestedQuantity <= availableQuantity
must be satisfied for successful consumption.
For cancellation:
consumed Inventory
→ restored
The Product entity should not own these rules.
Inventory owns Inventory state.
Product Availability vs Inventory Availability
We should avoid ambiguous fields such as:
boolean available;
What would available mean?
Product active?
Inventory > 0?
Enough Inventory for
this specific requested quantity?
These are different concepts.
Instead, our model stays explicit.
Product state:
active = true / false
Inventory state:
availableQuantity
Orderability depends on the relevant combination.
For Product browsing:
active
AND
availableQuantity > 0
For Order creation:
active
AND
availableQuantity >= requestedQuantity
This is clearer than hiding multiple meanings behind one boolean.
Order
Order is the central Domain Entity in our system.
An Order has:
- stable identity
- Customer ownership
- lifecycle state
- creation time
- OrderItems
- historical pricing
- total
- Payment-related state transition
- cancellation rules
Therefore:
Order is a Domain Entity.
Order Identity
Orders use UUID-based identity.
Conceptually:
OrderId
wraps
java.util.UUID
Once created:
Order O1
remains the same Order while its state changes.
For example:
UNPAID
↓
PAID
The identity remains unchanged.
Order Is More Than Data
A weak model would be:
class Order {
UUID id;
String customerId;
String status;
}
with arbitrary mutation:
order.setStatus(
"CANCELLED"
);
But our requirements say:
PAID Orders cannot be cancelled.
CANCELLED Orders cannot be paid.
Only UNPAID Orders can transition
to PAID or CANCELLED.
So the Domain should express meaningful operations.
Conceptually:
order.cancel();
and:
order.markPaid();
rather than a generic status setter.
An Entity should protect meaningful business state where appropriate.
Order Lifecycle
Our current v1 lifecycle is intentionally small:
UNPAID
PAID
CANCELLED
Valid transitions:
UNPAID
├── successful Payment ──▶ PAID
└── cancellation ────────▶ CANCELLED
Invalid:
PAID
──X──▶ CANCELLED
and:
CANCELLED
──X──▶ PAID
PAID and CANCELLED are terminal states in v1.
We do not add:
PROCESSING
PACKED
SHIPPED
DELIVERED
RETURNED
REFUNDED
because none of them belongs to our current requirements.
Why UNPAID Is Better Than PENDING
A vague state such as:
PENDING
could mean:
pending Payment
pending approval
pending fulfilment
pending processing
Our current meaning is specific:
The Order exists, but successful Payment has not been applied.
So:
UNPAID
communicates the Domain state more clearly.
OrderItem
An Order may contain multiple Products.
But an Order cannot simply keep Product references.
Each purchased line needs:
ProductId
quantity
purchase-time unitPriceCents
That concept is:
OrderItem
Why Purchase-Time Price Belongs to OrderItem
Suppose:
Product P1
priceCents = 2000
Customer creates:
Order O1
P1 × 2 @ 2000 cents
Later:
Product P1
priceCents = 2500
Historical O1 must still represent:
P1 × 2 @ 2000 cents
A new Order may use:
P1 × 2 @ 2500 cents
Therefore:
Product.priceCents
=
current catalog price
while:
OrderItem.unitPriceCents
=
price at Order creation
These are different facts.
Historical price storage is not meaningless duplication.
It preserves business history.
Should We Snapshot Product Name?
Our current requirement explicitly requires historical pricing.
It does not require historical Product-name snapshots.
Therefore we do not invent:
purchaseTimeProductName
or copy the full Product into the Order.
Current historical facts are limited to what the current requirements justify.
If future requirements need the original Product name or description, the design can evolve.
Does OrderItem Need Independent Identity?
Our current business behaviour does not allow users to:
fetch one OrderItem independently
cancel one OrderItem independently
pay one OrderItem independently
modify one OrderItem independently
An OrderItem exists as part of an Order.
Conceptually:
Order
├── OrderItem
├── OrderItem
└── OrderItem
Its lifecycle is tied to the Order.
Therefore we do not need a separate Domain identity such as:
OrderItemId
for v1.
OrderItem Persistence Identity
Even persistence does not require us to invent an OrderItemId.
Our current design can identify an item using:
(order_id, product_id)
because the same Product may appear at most once in one Order.
Conceptually:
OrderItem identity within persistence
=
OrderId + ProductId
This matches our duplicate-Product rule naturally.
Order Owns Its Items
This ownership matters.
Suppose external code could arbitrarily change:
quantity = 2
to:
quantity = 5
without the Order controlling its items.
Now Order total and historical meaning could become inconsistent.
Therefore:
Order owns its OrderItems and protects Order-level consistency.
This makes Order a natural aggregate root.
Aggregate Thinking
We do not need a full Domain-Driven Design course here.
But the idea of an Aggregate is useful.
An Aggregate defines a consistency boundary around related Domain objects.
Our natural Aggregate is:
Order
└── OrderItems
Order is the root.
Application behaviour should work through the Order rather than treating OrderItems as independently mutable objects.
Product Is Not Part of the Order Aggregate
OrderItem references Product.
But Order does not own Product.
Product:
- exists before the Order
- may appear in many Orders
- changes independently
- has its own lifecycle
Therefore:
OrderItem
↓ references
Product
not:
Order
↓ owns
Product
Inventory Is Not Part of the Order Aggregate
Inventory also exists independently.
It can be modified by:
Order creation
Order cancellation
Administrator quantity setting
So Order does not own Inventory either.
Order creation coordinates:
Product
Inventory
Order
but these remain separate Domain concepts.
Why Ownership Matters
If Inventory were modeled inside Order:
Order
├── OrderItems
└── Inventory
we would imply that one Order owns Inventory lifecycle.
That would be incorrect.
Likewise, copying Product into Order as a mutable owned Entity would confuse:
current catalog state
with:
historical transaction state
Clear ownership prevents these modeling errors.
Order Total
Order total does not need independent identity.
It is derived from:
Σ(
OrderItem.unitPriceCents
×
OrderItem.quantity
)
Conceptually:
itemTotal =
Math.multiplyExact(
unitPriceCents,
quantity
);
and:
orderTotal =
Math.addExact(
previousTotal,
itemTotal
);
Important invariant:
Order total must remain consistent
with its OrderItems.
Our current design derives the total rather than persisting a separate authoritative orders.total_cents field.
That avoids having two sources of truth.
Quantity
Quantity is value-like.
Current rule:
quantity >= 1
For Inventory consumption:
requested quantity
<=
available quantity
The simplest Java representation is:
int quantity;
We could introduce a Quantity value object.
But we will not do so unless it provides enough practical value to justify the abstraction.
Price and Money
Money requires exact arithmetic.
Our canonical representation is:
Java
→ long cents
PostgreSQL
→ BIGINT
For example:
2000
→ 20.00 currency units
and:
8990
→ 89.90 currency units
We do not use:
double
or:
float
for authoritative money.
We also do not use BigDecimal in this project because our current pricing model is whole minor units.
There is no multi-currency model in scope yet.
So we also avoid a generic:
Money(amount, currency)
framework.
Product Price
Product owns its current price:
long priceCents;
Rule:
priceCents >= 0
Zero is valid.
For example:
priceCents = 0
may represent a free Product.
OrderItem Price
OrderItem owns its historical:
long unitPriceCents;
Rule:
unitPriceCents >= 0
Once the Order has been successfully created, this value does not change merely because Product price changes later.
Payment
Payment is important, but its exact persistent Domain model is intentionally not finalized here.
Our requirements currently tell us:
Customer can pay an eligible Order.
Confirmed successful Payment
makes the Order PAID.
Failed Payment does not
make the Order PAID.
A successful Payment outcome
must not be applied twice.
Actual Payment execution belongs to an external provider.
Our application cares about:
Which Order is being paid?
Was Payment confirmed?
Was the result already applied?
What provider information is
needed for safe integration?
Do We Need a Payment Entity?
Possibly later.
But we should not create one before the integration requirements justify it.
A future design might require:
Payment
or:
PaymentAttempt
if provider behaviour introduces concepts such as:
attempt 1 → timeout
attempt 2 → success
But we have not designed that workflow yet.
So our current decision is:
Payment is a separate integration concern, but its exact persistent model is deferred until the Payment module.
This is better than inventing a sophisticated Payment subsystem now.
Why paid = true Is Too Weak as the Domain Model
The simplest representation might be:
boolean paid;
But our Order already has lifecycle state:
UNPAID
PAID
CANCELLED
So a second boolean could create contradictory state:
status = CANCELLED
paid = true
unless carefully coordinated.
The Order status already expresses the business lifecycle we currently need.
Provider-specific Payment state can be modeled later when actual integration requirements are known.
Order and Payment Are Different Concepts
Order represents the commercial transaction.
Payment represents an external financial interaction related to that Order.
Confirmed Payment may trigger:
Order
UNPAID → PAID
But Payment-provider details should not become part of Order's internal business model.
This keeps external integration concerns isolated.
Current Ownership Model
Our conceptual ownership now looks like this.
External Identity Capability
Owns:
authentication
identity lifecycle
Product
Owns:
ProductId
current Product data
current priceCents
active/inactive state
Inventory
Owns:
available quantity
for one ProductId
Order
Owns:
OrderId
CustomerId ownership
OrderStatus
createdAt
OrderItems
historical Order meaning
Order-level invariants
OrderItem
Owned by Order.
Represents:
ProductId
quantity
purchase-time unitPriceCents
Payment Integration
Owns:
communication with
the external Payment provider
and later whatever application-side Payment state the integration actually requires.
Current Domain Relationships
Conceptually:
CustomerId
│
│ owns
▼
Order
│
│ contains
▼
OrderItems
│
│ reference
▼
Product
│
│ associated with
▼
Inventory
Payment interaction:
Order
│
│ Payment workflow
▼
PaymentService
│
▼
External Payment Provider
This is a Domain-level view.
It is not yet a database schema or package diagram.
Domain Relationships Are Not Automatically JPA Relationships
Conceptually:
Order contains OrderItems.
That does not mean we immediately choose:
@OneToMany
Likewise:
OrderItem references Product.
does not mean we automatically choose:
@ManyToOne
The persistence design must consider:
loading behaviour
coupling
query patterns
historical requirements
JPA behaviour
In some cases, storing identifiers directly may be preferable.
We will decide that later.
Avoid JPA-Driven Domain Design
Bad sequence:
Which @OneToMany do I need?
↓
Build the Domain model
Better:
What business relationship exists?
↓
Who owns the lifecycle?
↓
What consistency is required?
↓
How should it be persisted?
↓
Choose JPA mapping
Persistence should support the Domain model rather than define it prematurely.
Current Domain Invariants
Let's collect the important rules.
Product
Product has stable identity.
Product name must be valid.
priceCents cannot be negative.
Product has an active/inactive state.
Inactive Products cannot be used
for new Orders.
Inventory
Inventory is identified by ProductId.
availableQuantity cannot be negative.
Successful Order creation cannot consume
more Inventory than is available.
Order
Order has stable identity.
Order belongs to one CustomerId.
Order contains at least one OrderItem.
Order has a valid lifecycle state.
New Orders begin as UNPAID.
PAID Orders cannot be cancelled in v1.
CANCELLED Orders cannot become PAID.
OrderItem
OrderItem belongs to an Order.
OrderItem references a ProductId.
Quantity is positive.
unitPriceCents is non-negative.
Purchase-time price is preserved.
At Order request level:
The same Product may appear
at most once in one Order.
Order Total
Order total must equal
the sum of item totals.
Customer Ownership
Customer-scoped operations may affect
only Orders belonging to that Customer.
Payment
Only an UNPAID Order is payable.
Failed Payment does not make
the Order PAID.
A confirmed successful Payment
must not be applied twice.
The exact external-integration implementation comes later.
Some Rules Cross Domain Boundaries
Not every rule belongs entirely inside one Entity.
For example:
requested quantity
<=
available Inventory
requires both Order-request information and current Inventory state.
Another example:
Order cancellation
→ restore Inventory
crosses Order and Inventory.
These behaviours require application-level UseCases.
Domain Behaviour vs Workflow Coordination
A useful distinction is:
Domain Entity
Protects rules about its own state.
For example:
order.cancel();
may protect:
PAID cannot become CANCELLED.
CANCELLED cannot be cancelled again.
Inventory can protect:
quantity cannot become negative
for local in-memory behaviour.
Application UseCase
Coordinates multiple concepts and dependencies.
For cancellation:
Load Customer-owned Order
↓
Ask Order to cancel
↓
Restore consumed Inventory
↓
Persist changes
The Order itself should not know about:
Repository
PostgreSQL
HTTP
Spring Security
Payment provider
Those concerns live outside the Domain Entity.
Avoid Repository Calls Inside Entities
Bad direction:
class Order {
private OrderRepository orderRepository;
private InventoryRepository inventoryRepository;
void cancel() {
// business logic
orderRepository.save(this);
inventoryRepository.restore(...);
}
}
Now the Domain Entity owns infrastructure and orchestration.
A cleaner model is:
Order
↓
protects Order rules
while:
CancelOrderUseCase
↓
coordinates Order
+
Inventory
+
Repositories
This produces clearer responsibilities and easier tests.
Order Creation Workflow
Our Domain model implies an application workflow such as:
Receive CustomerId
↓
Validate requested items
↓
Load Products
↓
Verify Product state
↓
Consume Inventory safely
↓
Read current Product prices
↓
Construct Order + OrderItems
↓
Persist Order
Entities protect local rules.
The UseCase coordinates them.
The database transaction later guarantees that local multi-state mutation remains atomic.
Cancellation Workflow
Conceptually:
Load Customer-owned Order
↓
Validate through Order.cancel()
↓
Restore consumed Inventory
↓
Persist atomically
The key distinction is:
Order owns Order-state rules. The UseCase owns cross-concept coordination.
Payment Workflow
Conceptually:
Load Customer-owned Order
↓
Validate that Order is payable
↓
Call PaymentService
↓
Process confirmed result
↓
Order.markPaid()
But the provider is external.
Therefore a local database transaction cannot make the entire workflow atomic.
That problem belongs to the dedicated Payment module.
Make Invalid State Harder to Represent
A useful design goal is:
Make invalid state difficult to create.
Instead of allowing:
new Order(
emptyItems
);
and hoping another layer checks it, Order construction should reject an empty item collection.
Instead of:
inventory.setAvailableQuantity(
-5
);
Inventory should reject invalid quantity.
Instead of:
order.setStatus(
OrderStatus.PAID
);
from anywhere, expose meaningful transitions such as:
order.markPaid();
This is practical encapsulation.
API Validation Still Matters
Domain validation does not replace transport validation.
Suppose a client sends:
{
"items": null
}
The HTTP boundary can reject malformed structure immediately.
Conceptually:
HTTP Validation
↓
Structurally valid input
↓
UseCase / Domain rules
↓
Valid business operation
Different boundaries protect different concerns.
Avoid Setter-Driven Domain Models
A weak Domain model often exposes:
order.setStatus(...);
product.setActive(...);
inventory.setAvailableQuantity(...);
for every state change.
That allows callers to manipulate state without expressing intent.
More meaningful operations might be:
order.cancel()
order.markPaid()
product.deactivate()
inventory.setAvailableQuantity(...)
inventory.decrease(...)
inventory.increase(...)
Not every property needs a business method.
But important state transitions should communicate meaning.
Avoid Unnecessary Inheritance
All our Entities have identifiers.
That does not justify:
Product extends BaseCommerceEntity
Order extends BaseCommerceEntity
Inventory extends BaseCommerceEntity
Shared technical fields do not imply a useful business is-a relationship.
Prefer explicit Domain concepts and composition.
Order and OrderItem Use Composition
The relationship is naturally:
Order
contains
OrderItems
OrderItems participate in:
historical record
total calculation
Order consistency
They do not require independent API or application lifecycle.
We should not create:
OrderItemHandler
OrderItemUseCase
just because an order_items table will exist later.
Orders Are Not Shopping Carts
Our requirement says:
Customer places an Order.
It does not include:
Cart
Add item later
Remove item later
Change quantity after Order creation
Therefore OrderItems should not be modeled like editable Cart lines.
If a future requirement introduces a Cart, it becomes a separate Domain concept.
Product Deactivation vs Deletion
Our current Product lifecycle uses deactivation rather than physical deletion.
Conceptually:
ACTIVE
↓
INACTIVE
Future Orders:
cannot use the Product
Historical Orders:
remain intact
We do not expose Product deletion because current requirements do not need it.
Historical Meaning Belongs to the Order
Once created, an Order represents a historical business event.
Later Product changes should not rewrite that event.
Our current requirement explicitly protects:
purchase-time unit price
This leads to a useful mindset:
Order is a historical business record, not a live projection of the current Product catalog.
We still snapshot only the information the current requirements justify.
Inventory Consumption Terminology
Earlier material sometimes used:
reserve Inventory
For v1, that is not our model.
Our agreed behaviour is:
Order creation
→ consumes available Inventory
and:
eligible UNPAID cancellation
→ restores consumed Inventory
We do not create:
InventoryReservation
or a reservation lifecycle.
A reservation subsystem would introduce additional concerns such as:
expiry
timeouts
background cleanup
reservation ownership
None is currently required.
Failed Payment Does Not Cancel an Order
Current behaviour:
Payment fails
↓
Order remains UNPAID
We have not established:
Payment fails
↓
Order becomes CANCELLED
So we do not invent that transition.
If the business later wants automatic cancellation, it becomes a separate requirement.
Current Domain Decisions
We can now summarize the Domain model.
Product
Domain Entity
Identity:
ProductId(UUID)
Owns:
- current name
- current priceCents
- active/inactive state
Inventory
Stateful Domain concept
Identity:
ProductId
Owns:
- current availableQuantity
- quantity invariant
Customer Identity
Externally owned identity
Represented locally as:
CustomerId
Used for:
- Order ownership
- Customer authorization
No Customer account subsystem is created.
Order
Domain Entity
Aggregate Root
Identity:
OrderId(UUID)
Owns:
- CustomerId
- OrderStatus
- createdAt
- OrderItems
- historical Order meaning
- Order-level invariants
OrderItem
Owned by Order
No independent Domain ID
Represents:
- ProductId
- quantity
- unitPriceCents
Persistence can identify it using:
OrderId + ProductId
Payment
External integration concern
Application-facing boundary:
PaymentService
Known responsibility:
- communicate with external provider
- return the outcome needed by PayOrderUseCase
Exact persistent Payment model:
deferred
Conceptual Domain Diagram
┌────────────────────┐
│ CustomerId │
└─────────┬──────────┘
│ owns
▼
┌────────────────────┐
│ Order │
│ │
│ OrderId │
│ status │
│ createdAt │
└─────────┬──────────┘
│ owns
▼
┌────────────────────┐
│ OrderItem │
│ │
│ ProductId │
│ quantity │
│ unitPriceCents │
└─────────┬──────────┘
│ references
▼
┌────────────────────┐
│ Product │
│ │
│ ProductId │
│ name │
│ priceCents │
│ active │
└─────────┬──────────┘
│ associated with
▼
┌────────────────────┐
│ Inventory │
│ │
│ ProductId │
│ availableQuantity │
└────────────────────┘
Payment remains outside the Order Aggregate:
Order
│
│ Payment workflow
▼
PayOrderUseCase
│
▼
PaymentService
│
▼
External Payment Provider
Review Against Requirements
Let's verify the model against the requirements.
Browse Products
Needs:
Product
Inventory
Covered.
Create Order
Needs:
CustomerId
Product
Inventory
Order
OrderItem
Covered.
Historical Pricing
Needs:
OrderItem.unitPriceCents
Covered.
Order History
Needs:
Customer ownership
Order
OrderItems
historical pricing
createdAt
Covered.
Cancellation
Needs:
Order lifecycle
Customer ownership
Inventory restoration
Covered.
Payment
Needs:
Order lifecycle
Customer ownership
PaymentService
Covered at the level currently justified.
Product Administration
Needs:
Product lifecycle
Covered.
Inventory Administration
Needs:
Inventory
Covered.
What We Deliberately Did Not Model
We did not introduce:
Customer Entity
Cart
Shipment
Warehouse
Discount
Coupon
Refund
Return
Supplier
Category
Recommendation
Invoice
InventoryReservation
Event Bus
PaymentAttempt
because none is currently required.
That is good Domain modelling.
The best model is not the one with the most concepts. It is the smallest model that accurately represents the current business problem.
Open Design Questions
Some questions intentionally remain:
How will Product and Inventory
map to persistence?
How will the Order Aggregate
map to JPA?
Should JPA relationships be used
or should IDs be stored directly?
How will Repository contracts look?
How will Order creation consume
Inventory safely under concurrency?
How will cancellation lock
and mutate Order state safely?
What exact Payment records will be needed?
How will Payment idempotency work?
These are not missing because we forgot them.
They belong to later persistence, transaction, concurrency, security, and Payment design.
Engineering Principle
The central principle from this lesson is:
A Domain Entity exists because its identity, lifecycle, and behaviour matter to the business—not because we need another Java class or database table.
Another important principle:
Ownership must be explicit. Order owns its OrderItems, but it does not own Product, Inventory, Customer identity, or the external Payment provider.
And finally:
Model only the concepts required by the current problem.
Summary
In this lesson, we established that:
- A Domain model represents business identity, state, relationships, rules, and behaviour.
- Domain Entity and JPA
@Entityare different concepts. - Product is a Domain Entity with UUID identity, current price, and active/inactive state.
- Inventory is a separate stateful concept because its lifecycle and mutation patterns differ from Product.
- Inventory is identified by
ProductId; it does not need a separateInventoryId. - Customer identity is required for ownership, but authentication and Customer account lifecycle remain outside our system.
CustomerIdis application context rather than a local Customer Entity.- Order is the central Domain Entity and the Aggregate Root for OrderItems.
- Order uses UUID identity.
- Order owns its lifecycle, Customer ownership, creation time, and OrderItems.
- OrderItem does not need an independent Domain identity.
- OrderItem persistence can use the composite identity
(OrderId, ProductId). - OrderItem stores Product reference, quantity, and purchase-time
unitPriceCents. - Current Product price and historical OrderItem price represent different facts.
- Money uses
longcents in Java andBIGINTin PostgreSQL. - Order total is derived from its items rather than maintained as a separate authoritative field.
- The Order lifecycle remains intentionally small:
UNPAID,PAID,CANCELLED. PAIDandCANCELLEDare terminal in v1.- Product and Inventory are not owned by Order.
- Cross-concept operations such as Order creation and cancellation belong in application UseCases.
- Domain Entities should not depend directly on Repositories, HTTP, PostgreSQL, Spring Security, or provider APIs.
- Payment remains an external integration concern behind
PaymentService; its exact persistent model is deferred. - Inventory is consumed during Order creation and restored during eligible cancellation; there is no reservation subsystem.
- Failed Payment leaves the Order
UNPAID; it does not automatically cancel the Order. - Cart, Warehouse, Refund, InventoryReservation, PaymentAttempt, and other unrelated concepts are not modeled without requirements.
- The goal is not to create the most sophisticated Domain model; it is to represent current business behaviour accurately.
Next lesson:
Defining System Responsibilities
There we will decide which responsibilities belong to Product, Inventory, Order, Payment, Security/Customer Context, UseCases, Repositories, and HTTP Handlers, so business logic does not collapse into controllers or giant internal Services.
