Designing the System

Identifying Domain Entities

LuettavaEsikatselu

Katselet ilmaista esikatseluoppituntia.

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:

Order is 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 @Entity are 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 separate InventoryId.
  • Customer identity is required for ownership, but authentication and Customer account lifecycle remain outside our system.
  • CustomerId is 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 long cents in Java and BIGINT in 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.
  • PAID and CANCELLED are 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.