Skip to main content

Introduction

The Cart system manages shopping carts for users in the ecommerce application. It provides automatic calculation of totals and discounts based on cart items, with support for product-level discounts and quantity-based pricing.
The Cart automatically recalculates totals and discounts whenever cart items are added, modified, or removed through JPA lifecycle hooks (@PrePersist and @PreUpdate).

Data Model

The Cart system consists of two main entities that work together:

Cart Entity

The primary shopping cart entity that belongs to a user and contains multiple cart items.
id
Long
required
Unique identifier for the cart (auto-generated)
user
User
required
Reference to the user who owns this cart. Maps to the customer_id foreign key column.
cartItems
List<CartItems>
required
Collection of line items in the cart. Lazily loaded with cascade operations enabled.
The cart items list is automatically initialized as an empty ArrayList. When setting cart items, the existing list is cleared and new items are added, triggering automatic recalculation.
total
BigDecimal
required
Total cart value before discounts are applied. Automatically calculated from all cart items.Validation: Must be greater than or equal to 0Calculation: Sum of (product price × quantity) for all items
discount
BigDecimal
Total discount amount applied to the cart. Automatically calculated based on product-level discounts.Validation: Cannot be negativeCalculation: Sum of per-item discounts, where each item discount = (product price × quantity × product discount percentage / 100), rounded to 2 decimal places using HALF_UP rounding
createdDate
LocalDateTime
Timestamp when the cart was first created. Set automatically and cannot be updated afterward.
lastModifiedDate
LocalDateTime
Timestamp of the most recent cart modification. Automatically updated on every save or update operation.

CartItems Entity

Individual line items within a cart, representing a specific product and its quantity.
id
Long
required
Unique identifier for the cart item (auto-generated using sequence strategy)
cart
Cart
required
Reference to the parent cart. This field is mandatory and cannot be null.
product
Product
required
Reference to the product in this line item. Lazily loaded and cannot be null.
quantity
int
required
Number of units of the product in the cart.Validation: Cannot be negative
Whenever the quantity is modified, the cart’s total and discount are automatically recalculated through the @PreUpdate hook.

Relationships

User (1) ──────┐

               ├─── (Many) Cart
               │              │
               │              │ (One)
               │              │
               │              ├─── (Many) CartItems
               │                            │
               │                            │ (Many)
               │                            │
Product (Many) ────────────────────────────┘
  • A User can have multiple Carts (though typically one active cart)
  • Each Cart belongs to exactly one User
  • A Cart contains many CartItems (one-to-many relationship with cascading)
  • Each CartItem references one Product and one Cart
  • A Product can appear in multiple CartItems across different carts

Automatic Calculation Logic

The Cart system automatically calculates totals and discounts using the calculateTotalAndDiscount() method, which is triggered in two scenarios:
  1. JPA Lifecycle Events: Before persisting or updating a cart (@PrePersist, @PreUpdate)
  2. Cart Items Update: When the setCartItems() method is called

Calculation Implementation

Here’s the exact calculation logic from the Cart entity (/home/daytona/workspace/source/src/main/java/com/example/ecommerce/cart/Cart.java:79-94):
void calculateTotalAndDiscount() {
    setTotal(BigDecimal.ZERO);
    setDiscount(BigDecimal.ZERO);

    if ((this.cartItems != null) && (!this.cartItems.isEmpty())) {
        for (CartItems cartItem : this.cartItems) {
            // Calculate line item total: price × quantity
            BigDecimal total = cartItem.getProduct().getPrice()
                .multiply(new BigDecimal(cartItem.getQuantity()));
            setTotal(getTotal().add(total));
            
            // Calculate line item discount: (price × quantity × discount%) / 100
            BigDecimal itemDiscount = total
                .multiply(cartItem.getProduct().getDiscount())
                .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);

            setDiscount(getDiscount().add(itemDiscount));
        }
    }
}

Calculation Steps

  1. Initialize: Both total and discount are reset to zero
  2. Validate: Check if cart items exist and list is not empty
  3. Iterate: For each cart item:
    • Line Total: Multiply product price by quantity
    • Add to Cart Total: Accumulate line total to cart total
    • Line Discount: Calculate (line total × product discount %) / 100
    • Rounding: Round discount to 2 decimal places using HALF_UP rounding mode
    • Add to Cart Discount: Accumulate line discount to cart discount
The BigDecimal.HALF_UP rounding mode rounds to the nearest neighbor, unless both neighbors are equidistant, in which case it rounds up. This ensures consistent and fair rounding for monetary calculations.

Example Calculation

Given a cart with the following items:
ProductPriceDiscount %QuantityLine TotalLine Discount
Product A$100.0010%2$200.00$20.00
Product B$50.0015%3$150.00$22.50
Product C$25.000%1$25.00$0.00
Cart Total: 200.00+200.00 + 150.00 + 25.00=25.00 = **375.00** Cart Discount: 20.00+20.00 + 22.50 + 0.00=0.00 = **42.50** Final Amount: 375.00375.00 - 42.50 = $332.50

Lifecycle Hooks

The Cart and CartItems entities use JPA lifecycle callbacks to ensure data consistency:

Cart Lifecycle

@PrePersist
@PreUpdate
private void onPersistorUpdate() {
    calculateTotalAndDiscount();
    this.lastModifiedDate = LocalDateTime.now();
}
  • Triggered before every save and update operation
  • Recalculates total and discount
  • Updates the lastModifiedDate timestamp

CartItems Lifecycle

@PrePersist
@PreUpdate
public void updateTotalAndDiscount() {
    this.getCart().calculateTotalAndDiscount();
}
  • Triggered when a cart item is created or modified
  • Propagates the recalculation to the parent cart
  • Ensures cart totals stay synchronized with item changes
Because of these lifecycle hooks, you never need to manually calculate totals or discounts. The system handles it automatically whenever cart data changes.

Validation Rules

The Cart system enforces the following validation constraints:

Cart Validations

  • total: Must be ≥ 0 (enforced by @Min(0))
  • discount: Must be ≥ 0 (enforced by @Min(0))
  • createdDate: Cannot be updated after creation (updatable = false)

CartItems Validations

  • quantity: Must be ≥ 0 (enforced by @Min(0))
  • cart: Cannot be null (optional = false)
  • product: Cannot be null (optional = false)

Best Practices

Use the setCartItems() method: When updating cart items, always use the setCartItems() method rather than directly manipulating the list. This method properly clears the existing items, adds new ones, and triggers recalculation.
Lazy Loading: Cart items and products use lazy loading (FetchType.LAZY). Ensure your persistence context is open when accessing these relationships to avoid LazyInitializationException.
Cascade Operations: The Cart entity has CascadeType.ALL configured for cart items. This means that persisting, updating, or removing a cart will cascade to all its items automatically.

Database Schema

The Cart system uses the following tables:

cart Table

ColumnTypeConstraints
idBIGINTPrimary Key, Auto-increment
customer_idBIGINTForeign Key → users(id)
totalDECIMALNOT NULL, CHECK (total >= 0)
discountDECIMALCHECK (discount >= 0)
created_dateTIMESTAMPNOT NULL
updated_atTIMESTAMP

cart_items Table

ColumnTypeConstraints
idBIGINTPrimary Key, Sequence
cart_idBIGINTForeign Key → cart(id), NOT NULL
product_idBIGINTForeign Key → product(id), NOT NULL
quantityINTEGERNOT NULL, CHECK (quantity >= 0)
The table name for users is users, carts is cart, and cart items is cart_items as specified by the @Table annotations.

Build docs developers (and LLMs) love