Shopping Cart Data Model
Overview
This document describes the data model for the Shopping Cart system in the Ink platform. The cart model is optimized for self-service ecommerce scenarios with real-time pricing, session persistence, and seamless checkout experiences.
Entity Relationship Diagram
Core Entities
Cart
The main shopping cart entity representing a customer's current shopping session.
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/entity/Cart.java
@Entity
@Table(name = "carts", indexes = {
@Index(name = "idx_cart_user", columnList = "user_id"),
@Index(name = "idx_cart_session", columnList = "session_id"),
@Index(name = "idx_cart_status", columnList = "status"),
@Index(name = "idx_cart_expires_at", columnList = "expires_at")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Cart {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user; // Null for guest carts
@Column(name = "session_id", length = 255)
private String sessionId; // Browser session ID for guest carts
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private CartStatus status = CartStatus.ACTIVE;
@Column(name = "subtotal", precision = 19, scale = 2)
private BigDecimal subtotal = BigDecimal.ZERO;
@Column(name = "discount_total", precision = 19, scale = 2)
private BigDecimal discountTotal = BigDecimal.ZERO;
@Column(name = "shipping_total", precision = 19, scale = 2)
private BigDecimal shippingTotal = BigDecimal.ZERO;
@Column(name = "tax_total", precision = 19, scale = 2)
private BigDecimal taxTotal = BigDecimal.ZERO;
@Column(name = "total", precision = 19, scale = 2)
private BigDecimal total = BigDecimal.ZERO;
@Column(name = "currency", length = 3)
private String currency = "USD";
@Column(name = "expires_at")
private Instant expiresAt;
@Column(name = "abandoned_at")
private Instant abandonedAt;
@Column(name = "completed_at")
private Instant completedAt;
@Column(name = "ip_address", length = 45)
private String ipAddress;
@Column(name = "user_agent", length = 500)
private String userAgent;
@Column(name = "referrer", length = 500)
private String referrer;
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CartItem> items = new ArrayList<>();
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CartDiscount> discounts = new ArrayList<>();
@Column(name = "metadata", columnDefinition = "jsonb")
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, Object> metadata = new HashMap<>();
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@Version
private Integer version;
@PrePersist
protected void onCreate() {
createdAt = Instant.now();
updatedAt = Instant.now();
if (expiresAt == null) {
expiresAt = Instant.now().plus(48, ChronoUnit.HOURS); // Default 48 hour expiration
}
}
@PreUpdate
protected void onUpdate() {
updatedAt = Instant.now();
}
// Business logic methods
public void addItem(CartItem item) {
items.add(item);
item.setCart(this);
recalculateTotals();
}
public void removeItem(CartItem item) {
items.remove(item);
item.setCart(null);
recalculateTotals();
}
public void recalculateTotals() {
this.subtotal = items.stream()
.map(CartItem::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
this.discountTotal = discounts.stream()
.map(CartDiscount::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// Tax and shipping calculated by external services
this.total = subtotal
.subtract(discountTotal)
.add(shippingTotal)
.add(taxTotal);
}
public boolean isExpired() {
return expiresAt != null && Instant.now().isAfter(expiresAt);
}
public boolean isAbandoned() {
return status == CartStatus.ABANDONED;
}
public boolean isActive() {
return status == CartStatus.ACTIVE && !isExpired();
}
public int getItemCount() {
return items.stream()
.mapToInt(CartItem::getQuantity)
.sum();
}
}
@Getter
public enum CartStatus {
ACTIVE("Active"),
CHECKOUT("Checkout In Progress"),
PAYMENT_PENDING("Payment Pending"),
PAYMENT_PROCESSING("Payment Processing"),
COMPLETED("Completed"),
ABANDONED("Abandoned"),
EXPIRED("Expired");
private final String displayName;
CartStatus(String displayName) {
this.displayName = displayName;
}
}
CartItem
Represents individual products added to the shopping cart.
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/entity/CartItem.java
@Entity
@Table(name = "cart_items", indexes = {
@Index(name = "idx_cart_item_cart", columnList = "cart_id"),
@Index(name = "idx_cart_item_product", columnList = "product_id")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CartItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cart_id", nullable = false)
private Cart cart;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@Column(name = "quantity", nullable = false)
private Integer quantity = 1;
@Column(name = "unit_price", nullable = false, precision = 19, scale = 2)
private BigDecimal unitPrice;
@Column(name = "discount_amount", precision = 19, scale = 2)
private BigDecimal discountAmount = BigDecimal.ZERO;
@Column(name = "total", precision = 19, scale = 2)
private BigDecimal total;
@OneToMany(mappedBy = "cartItem", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CartItemDiscount> discounts = new ArrayList<>();
@Column(name = "metadata", columnDefinition = "jsonb")
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, Object> metadata = new HashMap<>();
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@PrePersist
protected void onCreate() {
createdAt = Instant.now();
updatedAt = Instant.now();
calculateTotal();
}
@PreUpdate
protected void onUpdate() {
updatedAt = Instant.now();
calculateTotal();
}
public void calculateTotal() {
BigDecimal itemTotal = unitPrice.multiply(new BigDecimal(quantity));
this.total = itemTotal.subtract(discountAmount);
}
public void updateQuantity(int newQuantity) {
if (newQuantity <= 0) {
throw new IllegalArgumentException("Quantity must be greater than 0");
}
this.quantity = newQuantity;
calculateTotal();
}
}
CartDiscount
Represents cart-level discounts (coupon codes, promotions).
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/entity/CartDiscount.java
@Entity
@Table(name = "cart_discounts")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CartDiscount {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cart_id", nullable = false)
private Cart cart;
@Enumerated(EnumType.STRING)
@Column(name = "discount_type", nullable = false, length = 50)
private DiscountType discountType;
@Column(name = "code", length = 50)
private String code; // Coupon code if applicable
@Column(name = "amount", precision = 19, scale = 2)
private BigDecimal amount;
@Column(name = "percentage", precision = 5, scale = 2)
private BigDecimal percentage;
@Column(name = "description", length = 500)
private String description;
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@PrePersist
protected void onCreate() {
createdAt = Instant.now();
}
}
@Getter
public enum DiscountType {
COUPON_CODE("Coupon Code"),
PROMOTIONAL("Promotional Discount"),
VOLUME("Volume Discount"),
LOYALTY("Loyalty Reward"),
FIRST_PURCHASE("First Purchase Discount"),
SEASONAL("Seasonal Sale");
private final String displayName;
DiscountType(String displayName) {
this.displayName = displayName;
}
}
Data Transfer Objects (DTOs)
CartDto
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/dto/cart/CartDto.java
@Data
@Builder
public class CartDto {
private Long id;
private Long userId;
private String sessionId;
private CartStatus status;
private List<CartItemDto> items;
private List<CartDiscountDto> discounts;
private BigDecimal subtotal;
private BigDecimal discountTotal;
private BigDecimal shippingTotal;
private BigDecimal taxTotal;
private BigDecimal total;
private String currency;
private int itemCount;
private Instant expiresAt;
private Instant createdAt;
private Instant updatedAt;
}
@Data
@Builder
public class CartItemDto {
private Long id;
private Long productId;
private String productName;
private String productSku;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal discountAmount;
private BigDecimal total;
private String imageUrl;
}
@Data
@Builder
public class CartDiscountDto {
private Long id;
private DiscountType discountType;
private String code;
private BigDecimal amount;
private BigDecimal percentage;
private String description;
}
Request DTOs
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/dto/cart/AddToCartRequest.java
@Data
@Builder
public class AddToCartRequest {
@NotNull
private Long productId;
@Min(1)
private Integer quantity = 1;
private Map<String, Object> metadata;
}
@Data
@Builder
public class UpdateCartItemRequest {
@Min(1)
private Integer quantity;
}
@Data
@Builder
public class ApplyCouponRequest {
@NotBlank
private String couponCode;
}
@Data
@Builder
public class CheckoutRequest {
@NotNull
private ShippingAddressDto shippingAddress;
@NotNull
private BillingAddressDto billingAddress;
@NotNull
private PaymentMethodDto paymentMethod;
private String notes;
}
Database Schema
-- filepath: /Users/jetstart/dev/jetrev/ink/src/main/resources/db/changelog/schema/cart-schema.sql
-- Carts table
CREATE TABLE carts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id),
session_id VARCHAR(255),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
subtotal DECIMAL(19,2) DEFAULT 0,
discount_total DECIMAL(19,2) DEFAULT 0,
shipping_total DECIMAL(19,2) DEFAULT 0,
tax_total DECIMAL(19,2) DEFAULT 0,
total DECIMAL(19,2) DEFAULT 0,
currency VARCHAR(3) DEFAULT 'USD',
expires_at TIMESTAMP WITH TIME ZONE,
abandoned_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
ip_address VARCHAR(45),
user_agent VARCHAR(500),
referrer VARCHAR(500),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
version INTEGER DEFAULT 0
);
CREATE INDEX idx_cart_user ON carts(user_id);
CREATE INDEX idx_cart_session ON carts(session_id);
CREATE INDEX idx_cart_status ON carts(status);
CREATE INDEX idx_cart_expires_at ON carts(expires_at);
-- Cart items table
CREATE TABLE cart_items (
id BIGSERIAL PRIMARY KEY,
cart_id BIGINT NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL DEFAULT 1,
unit_price DECIMAL(19,2) NOT NULL,
discount_amount DECIMAL(19,2) DEFAULT 0,
total DECIMAL(19,2) NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT cart_item_unique_product UNIQUE(cart_id, product_id)
);
CREATE INDEX idx_cart_item_cart ON cart_items(cart_id);
CREATE INDEX idx_cart_item_product ON cart_items(product_id);
-- Cart discounts table
CREATE TABLE cart_discounts (
id BIGSERIAL PRIMARY KEY,
cart_id BIGINT NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
discount_type VARCHAR(50) NOT NULL,
code VARCHAR(50),
amount DECIMAL(19,2),
percentage DECIMAL(5,2),
description VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Cart item discounts table
CREATE TABLE cart_item_discounts (
id BIGSERIAL PRIMARY KEY,
cart_item_id BIGINT NOT NULL REFERENCES cart_items(id) ON DELETE CASCADE,
discount_type VARCHAR(50) NOT NULL,
amount DECIMAL(19,2) NOT NULL,
description VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Cart events for audit trail
CREATE TABLE cart_events (
id BIGSERIAL PRIMARY KEY,
cart_id BIGINT NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
event_type VARCHAR(50) NOT NULL,
description VARCHAR(500),
data JSONB,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_cart_event_cart ON cart_events(cart_id);
CREATE INDEX idx_cart_event_type ON cart_events(event_type);
Related Documentation
- Cart Overview
- Product Catalog
- Order Management
- JPA Best Practices
Next Steps: Implement Cart Service for business logic.