Implementing Business Workflows
Calculating Order Totals
আপনি একটি free preview lesson দেখছেন।
Order creation workflow-এ আমরা ইতিমধ্যে establish করেছি:
Client sends
→ ProductId
→ quantity
Backend decides
→ current Product price
→ OrderItem unit price
→ item total
→ Order total
এখন monetary calculation-টাকে production-friendlyভাবে define করব।
আমাদের canonical decision:
Money
→ long cents in Java
→ BIGINT in PostgreSQL
অর্থাৎ:
€89.90
→ 8990 cents
€0.00
→ 0 cents
€125.50
→ 12550 cents
Java field names সবসময় unit explicit করবে:
priceCents
unitPriceCents
totalCents
Database columns:
price_cents
unit_price_cents
এই lesson-এর core principle:
Money should have one unambiguous representation throughout the backend. In this application, that representation is whole cents stored as integers.
Why Use Cents?
Monetary values নিয়ে একটি common mistake:
double price = 89.90;
double binary floating-point representation ব্যবহার করে।
Financial values-এর authoritative calculation-এর জন্য আমরা সেটা চাই না।
আমাদের model:
long priceCents = 8990L;
এখানে:
8990
exact integer।
No floating-point approximation.
Why long, Not int?
Java int maximum approximately:
2.1 billion
cents।
That is roughly:
21 million currency units
একটি individual Product-এর জন্য অনেক মনে হতে পারে।
কিন্তু totals can grow through:
unit price
×
quantity
×
multiple OrderItems
এবং system-level monetary values-এর জন্য long gives much more headroom।
So:
long
is the appropriate default।
Why Explicit Cents in the Name?
Consider:
long price = 8990;
What does 8990 mean?
Could be:
€8,990
or:
€89.90
or some other unit।
Instead:
long priceCents = 8990L;
makes the representation explicit.
The same rule applies everywhere:
product.priceCents()
orderItem.unitPriceCents()
orderItem.totalCents()
order.totalCents()
Avoid ambiguous monetary variable names when the numeric unit matters.
Product Price
Our Product domain now conceptually becomes:
public final class Product {
private final ProductId id;
private String name;
private long priceCents;
private boolean active;
// ...
}
Creation:
public static Product create(
ProductId id,
String name,
long priceCents
) {
return new Product(
id,
name,
priceCents,
true
);
}
Price invariant:
priceCents >= 0
Product Price Validation
private static long requireValidPriceCents(
long priceCents
) {
if (priceCents < 0) {
throw new IllegalArgumentException(
"Product price must not be negative"
);
}
return priceCents;
}
Then:
public void changePrice(
long priceCents
) {
this.priceCents =
requireValidPriceCents(
priceCents
);
}
Zero remains valid:
priceCents = 0
because our existing requirement is:
price >= 0
not:
price > 0
Database Product Price
Our Product schema should now use:
CREATE TABLE products (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
price_cents BIGINT NOT NULL,
active BOOLEAN NOT NULL,
CONSTRAINT products_price_non_negative
CHECK (price_cents >= 0)
);
This replaces the earlier:
price NUMERIC
design।
Going forward:
price_cents BIGINT
is canonical।
API Representation
If our backend contract uses cents directly, Product creation becomes:
{
"name": "Mechanical Keyboard",
"priceCents": 8990
}
Response:
{
"id": "9ed4c9a9-8761-4e4f-9f50-aef83f14c13d",
"name": "Mechanical Keyboard",
"priceCents": 8990,
"active": true
}
This has an important advantage:
The API and backend use the same exact monetary representation.
No transport conversion from:
89.90
to:
8990
is required inside the backend.
Formatting Is a Presentation Concern
A frontend can display:
8990 cents
as:
89.90
in the relevant currency presentation।
But the backend's authoritative arithmetic stays:
whole cents
We do not mix:
decimal currency units
and:
minor units
inside the application.
Our Current Currency Scope
The application does not support:
multiple currencies
So we do not introduce:
Currency
Money
MonetaryAmount
frameworks or domain types yet।
The current contract is simply:
monetary values are whole cents
If multicurrency is ever introduced, monetary modeling would need to evolve deliberately.
OrderItem Captures Purchase-Time Price
Order creation loads current Product state:
Product
→ priceCents = 8990
Then creates:
new OrderItem(
product.id(),
quantity,
product.priceCents()
);
OrderItem stores:
unitPriceCents = 8990
This value is immutable historical data.
Updated OrderItem
package io.liveklass.ordermanagement.order.domain;
import io.liveklass.ordermanagement.product.domain.ProductId;
import java.util.Objects;
public final class OrderItem {
private final ProductId productId;
private final int quantity;
private final long unitPriceCents;
public OrderItem(
ProductId productId,
int quantity,
long unitPriceCents
) {
this.productId =
Objects.requireNonNull(
productId,
"productId must not be null"
);
if (quantity <= 0) {
throw new IllegalArgumentException(
"Quantity must be positive"
);
}
if (unitPriceCents < 0) {
throw new IllegalArgumentException(
"Unit price must not be negative"
);
}
this.quantity =
quantity;
this.unitPriceCents =
unitPriceCents;
}
public ProductId productId() {
return productId;
}
public int quantity() {
return quantity;
}
public long unitPriceCents() {
return unitPriceCents;
}
public long totalCents() {
return Math.multiplyExact(
unitPriceCents,
quantity
);
}
}
The important detail here is:
Math.multiplyExact(...)
rather than:
unitPriceCents * quantity
Why Math.multiplyExact()?
Normal Java integer arithmetic can overflow.
Suppose an unexpectedly large value reaches:
unitPriceCents * quantity
and the mathematical result is larger than long can hold।
Plain multiplication can wrap around into an incorrect value.
For money, silent overflow is unacceptable.
So:
Math.multiplyExact(
unitPriceCents,
quantity
);
throws:
ArithmeticException
instead of silently returning corrupted monetary data.
Overflow Is a System Safety Issue
We are not creating an arbitrary requirement like:
maximum Product price = X
or:
maximum quantity = 100
because those business limits have not been defined.
But we can still guarantee:
If arithmetic exceeds the numeric representation, the backend must fail rather than calculate the wrong amount.
That is a technical correctness property, not an invented business limit.
Order Total
Order total is:
sum of all OrderItem totals
Suppose:
Keyboard
unitPriceCents = 8990
quantity = 2
Subtotal:
17980 cents
Another item:
Dock
unitPriceCents = 6950
quantity = 1
Subtotal:
6950 cents
Order total:
24930 cents
Safe Order Total Calculation
We should also protect addition from overflow.
Avoid:
long total = 0;
for (OrderItem item : items) {
total += item.totalCents();
}
Instead:
public long totalCents() {
long totalCents = 0L;
for (OrderItem item : items) {
totalCents =
Math.addExact(
totalCents,
item.totalCents()
);
}
return totalCents;
}
Now both:
multiplication
and:
addition
are checked.
Why Not Streams Here?
We could write:
return items.stream()
.mapToLong(
OrderItem::totalCents
)
.reduce(
0L,
Math::addExact
);
That is valid.
But for beginner-facing domain logic, the explicit loop:
for (...)
makes:
checked accumulation
easier to see.
Choose readability over cleverness.
Total Is Derived State
We still do not add:
private long totalCents;
as a mutable independent Order property.
Why?
Because Order already contains:
OrderItems
and every OrderItem contains:
quantity
unitPriceCents
Therefore:
totalCents
is derivable.
If both are persisted independently:
OrderItems say 24930
but:
orders.total_cents says 25000
which one is correct?
We avoid that inconsistency by maintaining one source of truth.
Database Does Not Need orders.total_cents
Our schema remains:
orders
├── id
├── customer_id
├── status
└── created_at
No:
total_cents
column is required.
OrderItem table contains:
order_id
product_id
quantity
unit_price_cents
That is sufficient to reconstruct historical totals.
Updated OrderItem Schema
CREATE TABLE order_items (
order_id UUID NOT NULL,
product_id UUID NOT NULL,
quantity INTEGER NOT NULL,
unit_price_cents BIGINT NOT NULL,
CONSTRAINT order_items_pk
PRIMARY KEY (
order_id,
product_id
),
CONSTRAINT order_items_order_fk
FOREIGN KEY (order_id)
REFERENCES orders(id),
CONSTRAINT order_items_product_fk
FOREIGN KEY (product_id)
REFERENCES products(id),
CONSTRAINT order_items_quantity_positive
CHECK (quantity > 0),
CONSTRAINT order_items_unit_price_non_negative
CHECK (unit_price_cents >= 0)
);
No decimal monetary columns remain.
JPA Mapping
Product:
@Column(
name = "price_cents",
nullable = false
)
private long priceCents;
OrderItem:
@Column(
name = "unit_price_cents",
nullable = false
)
private long unitPriceCents;
Simple mapping:
Java long
↔
PostgreSQL BIGINT
No decimal precision/scale configuration is necessary.
No Rounding During Order Calculation
With our cents model:
unitPriceCents
is already a whole integer.
Quantity is also a whole integer.
Therefore:
unitPriceCents × quantity
produces another whole-cent amount.
There is no:
decimal scale
or:
rounding mode
inside Order total calculation.
This is one of the major benefits of the model.
Example
Product price:
1234 cents
Quantity:
3
Subtotal:
3702 cents
Exactly.
No question like:
Should 37.019999 round to 37.02?
can occur in this calculation.
Where Rounding Could Still Exist
If future requirements introduce:
percentage discounts
tax rates
currency conversion
then some operation may mathematically produce fractions of a cent.
At that point we would need an explicit rounding policy.
For example:
round half up
round half even
round per line
round at Order total
Those are business decisions.
Our current application has none of:
discount
tax
currency conversion
so no rounding policy is needed yet.
Do Not Invent One Preemptively
Avoid code such as:
roundMoney(...)
or:
MoneyUtils
when every monetary value is already an integer number of cents.
There is nothing to round in our current domain.
Client Must Never Supply Unit Price
Create Order request remains:
{
"items": [
{
"productId": "9ed4c9a9-8761-4e4f-9f50-aef83f14c13d",
"quantity": 2
}
]
}
Not:
{
"items": [
{
"productId": "...",
"quantity": 2,
"unitPriceCents": 100
}
]
}
The backend loads:
Product.priceCents
itself.
Client Must Never Supply Total
Also never:
{
"totalCents": 100
}
Order total is server-derived.
Even if a frontend displays:
249.30
before submission, that display is not authoritative.
The backend calculates again from current Product state.
Server Authority
The trust boundary is:
Client controls
→ desired Product IDs
→ quantities
Server controls
→ Product existence
→ Product active state
→ Inventory
→ priceCents
→ unitPriceCents
→ totalCents
→ OrderStatus
This prevents a client from creating:
€100 Product
for:
€1
by manipulating request JSON.
Browse Price vs Order Price
Suppose Customer browses Product:
{
"id": "...",
"name": "Mechanical Keyboard",
"priceCents": 8990
}
Before Order creation, admin changes price:
8990
→
9990
Customer submits:
ProductId
quantity
CreateOrderUseCase reloads Product and captures:
9990
The browsing response was informational, not a price lock.
Purchase-Time Price Is Immutable
After successful Order creation:
OrderItem.unitPriceCents = 9990
Later Product changes:
9990
→
10990
Existing OrderItem remains:
9990
because it records historical purchase-time price.
This is one of our most important Order invariants.
OrderResponse
Our Order API should reflect the cents representation consistently.
Example:
{
"id": "36a25516-2af2-4644-b55d-d0cabf389c9e",
"status": "UNPAID",
"items": [
{
"productId": "9ed4c9a9-8761-4e4f-9f50-aef83f14c13d",
"quantity": 2,
"unitPriceCents": 8990,
"totalCents": 17980
},
{
"productId": "e7065e8f-d5fc-4bf3-85c3-c42cf5ed4457",
"quantity": 1,
"unitPriceCents": 6950,
"totalCents": 6950
}
],
"totalCents": 24930
}
No decimal representation ambiguity exists.
ProductResponse Changes Too
Earlier examples used:
{
"price": 89.90
}
From now on, canonical API is:
{
"priceCents": 8990
}
Similarly Product request:
{
"name": "Mechanical Keyboard",
"priceCents": 8990
}
The earlier decimal examples should be considered superseded by this course decision.
Validation
Product request:
public record CreateProductRequest(
@NotBlank
String name,
@NotNull
@PositiveOrZero
Long priceCents
) {
}
Why Long?
Because we need to distinguish:
missing
from:
0
at the transport boundary.
Domain then accepts primitive:
long
after validation.
UpdateProductRequest
Likewise:
public record UpdateProductRequest(
String name,
@PositiveOrZero
Long priceCents
) {
}
For PATCH:
null
→ no price change
0
→ set Product price to zero
That distinction matters.
Product Domain Uses Primitive long
Inside the Domain:
private long priceCents;
A valid Product should never contain:
null price
Transport may temporarily represent absence through Long.
Domain does not.
This is a good example of transport representation differing from domain representation for a legitimate reason.
Database Constraint
PostgreSQL protects:
CHECK (price_cents >= 0)
and:
CHECK (unit_price_cents >= 0)
So even if application bugs attempted to persist negative monetary state, database integrity would reject it.
Again:
HTTP validation
→ early request protection
Domain
→ intrinsic business integrity
Database
→ persisted structural integrity
Should We Create a Money Class?
We could create:
public record Money(long cents) {
}
But our current system:
has one monetary unit representation
has no multicurrency
has very simple arithmetic
So introducing:
Money
is not necessary yet.
Explicit names:
priceCents
unitPriceCents
totalCents
are clear enough.
If monetary behaviour becomes more sophisticated later, a value object may become justified.
Why Not Price Value Object?
Same reasoning.
A Price type could provide:
non-negative invariant
but Product is currently the primary owner of Product-price validity.
OrderItem similarly protects unitPriceCents.
We avoid creating extra types merely to wrap one primitive unless they improve real domain clarity.
Integer Money Does Not Mean "No Domain Rules"
Using:
long
does not mean arbitrary values are acceptable.
Domain still enforces:
priceCents >= 0
and:
unitPriceCents >= 0
Representation and validity are different concerns.
OrderItem Total Cannot Be Negative
Because:
unitPriceCents >= 0
and:
quantity > 0
mathematically subtotal should be non-negative.
The only technical risk is numeric overflow.
That's why:
Math.multiplyExact(...)
is important.
Order Total Cannot Be Negative
Likewise all item totals are non-negative.
Checked addition means:
Math.addExact(...)
will either produce the exact valid total or fail.
We never silently produce:
negative total
because of integer overflow.
Arithmetic Failure
What should happen if:
Math.multiplyExact(...)
or:
Math.addExact(...)
throws?
That indicates values exceed our supported numeric representation.
This is not a normal business event such as:
INSUFFICIENT_INVENTORY
It indicates unexpected/unrepresentable application state.
It should not be converted into:
price = 0
or silently clamped.
Fail visibly.
Do Not Saturate Money Values
Avoid:
if (overflow) {
return Long.MAX_VALUE;
}
This would turn:
unknown/corrupt amount
into:
apparently valid monetary amount
which is worse.
Exact arithmetic or failure is preferable.
Do Not Use Math.abs() as Validation
This:
priceCents =
Math.abs(priceCents);
does not validate negative price.
It silently changes invalid input.
Correct:
negative
→ reject
Business systems should not repair monetary values by guessing what the client meant.
No double Conversion in the Middle
Avoid:
double price =
priceCents / 100.0;
then calculate:
double total =
price * quantity;
That defeats the entire integer-money model.
All authoritative calculations remain:
long cents
Only presentation may convert the value for human-readable display.
If the Backend Formats Money
If one day an email or document generated by the backend needs formatted currency text, formatting should happen at that boundary.
For example conceptually:
24930 cents
→ "249.30"
But Order domain still stores/calculates:
24930
Formatting is not business arithmetic.
Persistence Round Trip
Persistence integration tests should prove:
8990 cents
is stored as:
8990
in PostgreSQL and loaded back as:
8990L
No decimal conversion should occur anywhere.
Historical Price Persistence Test
A strong integration scenario:
- Create Product:
priceCents = 8990
- Create Order:
quantity = 2
-
Persist Order.
-
Update Product:
priceCents = 9990
- Reload Order.
Expected:
OrderItem.unitPriceCents = 8990
OrderItem.totalCents = 17980
Product current state:
9990
Order historical state:
8990
Both are correct.
Total Test
Given:
Item A
unitPriceCents = 8990
quantity = 2
and:
Item B
unitPriceCents = 6950
quantity = 1
Expected:
Item A total = 17980
Item B total = 6950
Order total = 24930
Unit test:
assertEquals(
24930L,
order.totalCents()
);
Zero-Price Product
Given:
unitPriceCents = 0
quantity = 5
Expected:
subtotal = 0
This is valid under our current rules.
Order can contain a zero-priced Product if that Product is otherwise valid and orderable.
We do not invent a minimum monetary amount.
Large Quantity Arithmetic
Even though API currently has no explicit maximum Order quantity, arithmetic must stay correct.
For example:
Math.multiplyExact(
unitPriceCents,
quantity
);
protects us from silent overflow.
A later product requirement may introduce quantity limits for:
business reasons
abuse prevention
inventory constraints
but that's separate from numeric correctness.
Total Calculation Does Not Query Product
Once Order is constructed, this would be wrong:
order.totalCents(
productRepository
);
or:
for every OrderItem
→ reload current Product price
Order total must depend on:
OrderItem.unitPriceCents
already captured at creation.
Otherwise historical Orders would change when Product price changes.
Total Calculation Does Not Query Database
order.totalCents() is pure domain logic.
It requires:
no Spring
no JPA
no PostgreSQL
no Repository
Given an Order's items, total can always be calculated.
This makes it easy to test and reason about.
Price Snapshot Is the Source of Historical Truth
The relationship is:
Product.priceCents
→ current catalog truth
OrderItem.unitPriceCents
→ historical Order truth
Do not confuse them.
No Product Name Snapshot Yet
We capture:
productId
unitPriceCents
but not:
productName
because current requirements do not require historical Product-name display.
If that requirement arrives, it may justify another snapshot field.
We do not add it merely because price is snapshotted.
No Currency Column Yet
Likewise we do not add:
currency
to Product or OrderItem because this v1 model is not multicurrency.
Once currency becomes a domain requirement, storing only:
8990
would no longer be enough without knowing the currency.
That is a future design change, not something we need to solve now.
Migration Adjustment
Because previous lessons used decimal monetary columns, our canonical schema now changes to:
products.price_cents BIGINT
and:
order_items.unit_price_cents BIGINT
If this were already a shared production migration history, we would create a new Flyway migration.
But in the course design we are correcting the model before treating those earlier decimal definitions as final implementation history.
The final course repository should have one internally consistent schema history using the approved cents model.
Repository Models Must Follow the Same Language
Avoid persistence models such as:
private long price;
Use:
private long priceCents;
Similarly SQL aliases, projections, and DTOs should use explicit terminology where practical.
Consistency reduces unit-conversion bugs.
API Error for Negative Price
Request:
{
"name": "Mechanical Keyboard",
"priceCents": -1
}
should produce:
400
VALIDATION_ERROR
Example field problem:
{
"field": "priceCents",
"code": "INVALID_VALUE",
"message": "Price must be greater than or equal to 0 cents."
}
The field name itself already tells the client the expected unit.
Don't Accept Both Formats
Avoid an API that accepts:
{
"price": 89.90
}
and alternatively:
{
"priceCents": 8990
}
Now there are two monetary representations and conversion paths.
Choose one.
Our v1 contract chooses:
cents
Don't Guess Client Units
If client sends:
{
"priceCents": 89
}
backend interprets that exactly as:
89 cents
It must not guess:
Maybe they meant 89 currency units.
Units are part of the API contract.
Naming Across the System
Use consistent language:
Product.priceCents
OrderItem.unitPriceCents
OrderItem.totalCents()
Order.totalCents()
products.price_cents
order_items.unit_price_cents
ProductResponse.priceCents
OrderItemResponse.unitPriceCents
OrderResponse.totalCents
This consistency is more valuable than saving a few characters in field names.
Common Mistake 1 — double for Money
Avoid floating-point monetary arithmetic.
Common Mistake 2 — Ambiguous long price
The numeric unit must be obvious.
Use:
priceCents
Common Mistake 3 — Client-Supplied Total
The server calculates totals.
Common Mistake 4 — Client-Supplied Unit Price
The server loads current Product price.
Common Mistake 5 — Recalculating Historical Order From Current Product Price
OrderItems retain purchase-time price.
Common Mistake 6 — Persisting Mutable Order Total Separately
Total remains derived from OrderItems.
Common Mistake 7 — Plain Integer Arithmetic Without Overflow Checks
Use:
Math.multiplyExact(...)
and:
Math.addExact(...)
for monetary totals.
Common Mistake 8 — Decimal Conversion During Business Logic
Stay in cents throughout authoritative calculations.
Common Mistake 9 — Inventing Rounding Rules
Current integer-cent arithmetic requires no rounding policy.
Common Mistake 10 — Adding Multicurrency Prematurely
No currency abstraction until the product requires multiple currencies or explicit currency modeling.
Monetary Model Checklist
For every monetary field, ask:
Is the unit explicit?
Is it represented as long cents?
Can it ever be negative?
Who controls the value?
Is it current state or historical state?
Is arithmetic checked for overflow?
Is the value derived or persisted?
Can a Product update mutate historical Order data?
Is the client allowed to supply it?
Our Canonical Monetary Model
Product:
priceCents: long
Database:
price_cents BIGINT
OrderItem:
unitPriceCents: long
Database:
unit_price_cents BIGINT
Item subtotal:
Math.multiplyExact(
unitPriceCents,
quantity
)
Order total:
sum using Math.addExact
No:
double
float
BigDecimal
NUMERIC
client total
persisted Order total
in our current monetary model.
Engineering Principle
The core principle:
Money must have one explicit unit throughout the system. Our application stores and calculates whole cents using
long, and PostgreSQL persists those values asBIGINT.
Another:
Current Product price and historical OrderItem price are different facts. The Order captures the server-side Product price at successful creation and never recalculates historical totals from current Product state.
And:
Integer money removes floating-point and rounding ambiguity from our current workflows, but arithmetic still needs overflow protection. Exact calculation or failure is preferable to silently corrupted monetary values.
Summary
In this lesson, we established that:
- All monetary values use whole cents.
- Java uses
longfor domain monetary values. - PostgreSQL uses
BIGINT. - Product price becomes
priceCents. - OrderItem purchase-time price becomes
unitPriceCents. - Derived monetary values use names such as
totalCents. - The API exposes cents rather than decimal currency-unit values.
- Product creation/update requests use
priceCents. - Zero-priced Products remain valid.
- Negative prices are rejected.
- Order creation never accepts a client-supplied price.
- Order creation never accepts a client-supplied total.
- OrderItem captures the current server-side Product
priceCents. - Historical OrderItem price does not change when Product price changes.
- Item total is
unitPriceCents × quantity. Math.multiplyExact()protects subtotal calculation from silent overflow.Math.addExact()protects Order total accumulation from silent overflow.- Order total remains derived rather than independently persisted.
- No rounding policy is needed for current whole-cent arithmetic.
- We do not introduce discounts, tax, currency conversion, or multicurrency.
- We do not introduce a
Moneyabstraction without a real need. - Database constraints reinforce non-negative monetary state.
- Product and OrderItem schema now use
price_cents BIGINTandunit_price_cents BIGINT. - Earlier
BigDecimal/NUMERICmonetary examples are superseded by this cents-based model. - Formatting a cents value into human-readable decimal currency belongs at a presentation boundary, not inside authoritative Order arithmetic.
Next lesson:
Checking and Consuming Inventory Safely
There we will address the first serious concurrency problem in our application: two Customers trying to buy the same remaining stock simultaneously. We will compare naive read-check-write, pessimistic row locking, optimistic locking, and conditional atomic SQL, then choose a concrete PostgreSQL strategy for our Order creation workflow.