Implementing Business Workflows

Calculating Order Totals

ReadingPreview

আপনি একটি 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:

  1. Create Product:
priceCents = 8990
  1. Create Order:
quantity = 2
  1. Persist Order.

  2. Update Product:

priceCents = 9990
  1. 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 as BIGINT.

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 long for 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 Money abstraction without a real need.
  • Database constraints reinforce non-negative monetary state.
  • Product and OrderItem schema now use price_cents BIGINT and unit_price_cents BIGINT.
  • Earlier BigDecimal / NUMERIC monetary 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.