Skip to main content

Cart Management Deep Dive

Overview

This comprehensive guide covers Shopping Cart Management in the Ink platform, including cart lifecycle, item management, pricing calculation, session persistence, and the transition from browsing to checkout. The cart system is optimized for self-service ecommerce scenarios enabling customers to independently select products, adjust quantities, and proceed to purchase.

Target Audience: Backend developers working with cart and ecommerce functionality
Prerequisites: Service Layer Architecture, Product Catalog
Estimated Time: 45-60 minutes

Prerequisites

  • Understanding of ecommerce shopping cart concepts
  • Knowledge of JPA and Spring Data
  • Familiarity with session management patterns
  • Completed Service Layer Architecture
  • Basic understanding of pricing and discount calculations

Cart Lifecycle Architecture

Cart Stage/Status Management

Cart Stage State Machine

Carts progress through distinct stages representing their lifecycle from creation through order fulfillment. Each stage determines which operations are permitted and which linked objects (licenses, orders, invoices) are associated with the cart.

Cart Stages Reference

StageDescriptionLinked ObjectsAllowed TransitionsBusiness Rules
DRAFTInitial cart creation state. Items can be added, removed, or modified. No licenses created yet.Cart, CartItemsPLACED, CANCELED- Cart must have at least one product to transition to PLACED
- Products can be freely modified
- No licenses or subscriptions exist
PLACEDOrder submitted. Trial licenses and subscription created. Awaiting payment or approval.Cart, CartItems, Licenses (Trial), Subscription, Opportunity (Salesforce)PAYMENT_IN_PROGRESS, AWAITING_ACCEPT, COMPLETED, CANCELED- Trial licenses created for all cart products
- Subscription created and linked
- Salesforce opportunity created if configured
- Cannot modify cart items
PAYMENT_IN_PROGRESSPayment processing in progress via Stripe or payment gateway.Cart, Licenses (Trial), Subscription, Payment (Pending)COMPLETED, CANCELED- Payment initiated with external gateway
- Awaiting payment confirmation
- Can timeout and revert to CANCELED
AWAITING_ACCEPTManual approval required before completion. Common for enterprise/custom orders.Cart, Licenses (Trial), Subscription, Opportunity (Won)COMPLETED, CANCELED- Requires manual review/approval
- Salesforce opportunity set to "Won" stage
- Held pending internal approval workflow
COMPLETEDPayment successful. Trial licenses converted to full licenses. Order can now be invoiced.Cart, Licenses (Full), Subscription (Active), Payment (Completed), Opportunity (Closed Won)DONE- Trial licenses converted to full licenses
- Subscriptions activated
- External systems notified
- Salesforce opportunity set to "Closed Won"
- Invoices can be generated
DONEAll invoices paid and transactions linked. Final state.Cart, Licenses (Full), Subscription (Active), Payment (Completed), Invoices (Paid), Transactions(Terminal)- All cart products have transaction IDs
- All invoices paid
- No further transitions allowed
- Cart archived
CANCELEDCart canceled or payment failed. Licenses deactivated.Cart, Licenses (Deactivated), Opportunity (Closed Lost)(Terminal)- All associated trial licenses deactivated
- Salesforce opportunity set to "Closed Lost"
- Cannot be reactivated
- Audit trail preserved

Stage Transition Rules

DRAFT → PLACED

  • Precondition: Cart must have at least one product
  • Actions:
    • Create subscription for cart
    • Create trial licenses for each cart product
    • Create Salesforce opportunity (if configured)
    • Link licenses to users (if specified)
    • Import projects (if specified)
  • Linked Objects Created: Subscription, Licenses (Trial), Opportunity

PLACED → COMPLETED

  • Precondition: Cart in PLACED, AWAITING_ACCEPT, or PAYMENT_IN_PROGRESS stage
  • Actions:
    • Convert trial licenses to full licenses
    • Set Salesforce opportunity to "Closed Won"
    • Handle installment payments (if applicable)
    • Notify external systems
  • Linked Objects Modified: Licenses (Trial → Full), Opportunity (→ Closed Won), Payment (→ Completed)

Any → CANCELED

  • Precondition: Cart in DRAFT, PLACED, or AWAITING_ACCEPT stage
  • Actions:
    • Deactivate all associated licenses
    • Set Salesforce opportunity to "Closed Lost"
    • Log cancellation event
  • Linked Objects Modified: Licenses (→ Deactivated), Opportunity (→ Closed Lost)

COMPLETED → DONE

  • Precondition: All cart products have transaction IDs assigned
  • Actions:
    • Verify all invoices paid
    • Finalize cart processing
  • Linked Objects Verified: Transactions, Invoices

Payment Status vs Cart Stage

Cart stages are distinct from payment status. The relationship is:

Cart StagePayment StatusDescription
DRAFTN/ANo payment initiated
PLACEDUNPAIDTrial licenses created, awaiting payment
PAYMENT_IN_PROGRESSIN_PROGRESSPayment processing
AWAITING_ACCEPTUNPAIDManual approval, payment pending
COMPLETEDPAIDPayment successful, licenses activated
DONEPAIDAll transactions complete
CANCELEDFAILED/UNKNOWNPayment failed or canceled

Note: Payment status is managed separately via the PaymentStatus enum (INITIALIZED, IN_PROGRESS, SUCCEEDED, CANCELED, PAST_DUE, FAILED, UNKNOWN) and is linked to cart stage transitions.

Installation Steps

The Installation Steps section establishes the shopping cart infrastructure for self-service ecommerce in the Ink platform. These steps configure cart persistence, session management, pricing calculation, and integration with the product catalog and checkout systems.

What You'll Set Up:

  1. Cart Dependencies - Core libraries for cart management and session handling
  2. Cart Configuration - Settings for cart expiration, pricing rules, and session persistence

Why This Matters:

  • Enables self-service product selection and purchasing
  • Provides seamless shopping experience across sessions
  • Integrates real-time pricing with the product catalog
  • Manages cart state and item quantities accurately
  • Supports promotional pricing and discount application
  • Facilitates smooth transition to checkout and order creation

When to Use:

  • Implementing ecommerce or self-service portals
  • Setting up in-app purchase capabilities
  • Building trial-to-paid conversion flows
  • Creating customer self-service experiences
  • Implementing cart abandonment recovery

Key Capabilities Enabled:

  • Item Management: Add, update, and remove cart items
  • Pricing Calculation: Real-time price updates with discounts
  • Session Persistence: Save cart state across user sessions
  • Quantity Management: Handle stock availability and limits
  • Cart Abandonment: Track and recover abandoned carts
  • Checkout Integration: Seamless transition to order processing

Configuration Hierarchy:

cart
├── session
│ ├── timeout: 30m
│ ├── persistenceEnabled: true
│ └── cleanupSchedule: "0 0 2 * * ?"
├── items
│ ├── maxQuantityPerItem: 999
│ ├── maxItemsPerCart: 100
│ └── autoRemoveOutOfStock: true
├── pricing
│ ├── realTimeUpdates: true
│ ├── applyDiscounts: true
│ └── includeTax: false
└── abandonment
├── abandonmentThresholdDays: 30
├── recoveryEmailEnabled: true
└── recoveryEmailDelayHours: 24

1. Cart Dependencies

What Are Cart Dependencies?

The cart module relies on Spring Boot starters for web and data persistence, session management libraries, and internal common utilities for pricing and product integration.

Why Are They Needed?

  • Persistence: JPA/Hibernate for cart and item storage
  • Session Management: Spring Session for cart state across requests
  • Web: REST API for cart operations
  • Validation: Bean validation for cart business rules
  • Pricing Integration: Product catalog and pricing engine clients
  • Internal: Common DTOs and utilities

How/When Are They Used?

  • Build Time: Compiled into the cart service
  • Runtime: Loaded by Spring container for request handling
  • Session Handling: Maintains cart state between user requests
  • Integration: Connects to product and pricing services

Dependency Relationships

Code Example (XML)

<!-- filepath: /sureink-cloud-msa/pom.xml -->
<dependencies>
<dependency>
<groupId>com.sureink</groupId>
<artifactId>sureink-model</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.sureink</groupId>
<artifactId>sureink-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
</dependencies>

2. Cart Configuration

What Is Cart Configuration?

Configuration settings control cart behavior including session timeouts, item limits, pricing rules, and abandonment tracking policies.

Why Is It Needed?

  • Session Management: Define cart lifespan and persistence
  • Business Rules: Enforce quantity limits and item constraints
  • Pricing Control: Enable real-time pricing and discount application
  • Recovery: Configure cart abandonment detection and recovery
  • Performance: Tune cache settings and cleanup schedules

How/When Is It Used?

  • Startup: Loaded during application bootstrap
  • Runtime: Referenced by cart services for business logic
  • Scheduled Tasks: Cleanup abandoned carts based on configuration
  • Session Management: Control cart persistence duration

Configuration Flow

Code Example (YAML)

# filepath: /sureink-cloud-msa/src/main/resources/application.yml
cart:
session:
timeout: 30m # Cart session timeout
persistenceEnabled: true # Persist cart across sessions
cleanupSchedule: "0 0 2 * * ?" # Clean abandoned carts daily at 2 AM

items:
maxQuantityPerItem: 999 # Maximum quantity per line item
maxItemsPerCart: 100 # Maximum unique items in cart
autoRemoveOutOfStock: true # Remove items if out of stock

pricing:
realTimeUpdates: true # Recalculate prices on every change
applyDiscounts: true # Apply available discounts
includeTax: false # Calculate tax separately
cachePricingSeconds: 300 # Cache pricing for 5 minutes

abandonment:
abandonmentThresholdDays: 30 # Mark cart abandoned after 30 days
recoveryEmailEnabled: true # Send recovery emails
recoveryEmailDelayHours: 24 # Wait 24 hours before recovery email
maxRecoveryAttempts: 3 # Send up to 3 recovery emails

Configuration Section

Main Entity: Cart

What Is the Cart Entity?

The Cart entity is the root aggregate for shopping cart data. It stores cart metadata, customer reference, session information, and links to cart items.

Why Is It Needed?

  • State Management: Tracks cart lifecycle and status
  • Session Association: Links cart to user or anonymous session
  • Item Container: Aggregates all items in the cart
  • Pricing: Stores calculated totals and discounts
  • Audit: Tracks cart creation and modification history

How/When Is It Used?

  • Session Creation: New cart created when user starts shopping
  • Item Operations: Updated when items are added/removed/modified
  • Checkout: Read during checkout process
  • Abandonment: Queried for abandoned cart recovery
  • Analytics: Analyzed for shopping behavior insights

Entity Relationships

Code Example (Java)

// filepath: /sureink-model/src/main/java/com/sureink/cart/model/Cart.java
@Entity
@Table(schema = "cloudmsa", name = "cart")
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@Getter
@Setter
public class Cart {

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CART_SEQUENCE")
@SequenceGenerator(name = "CART_SEQUENCE", sequenceName = "cart_sequence", schema = "cloudmsa", allocationSize = 25)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;

@Column(name = "session_id", length = 255)
private String sessionId;

@Enumerated(EnumType.STRING)
@Column(name = "status", length = 20)
private CartStatus status;

@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;

@Column(name = "expires_at")
private LocalDateTime expiresAt;

@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("id ASC")
private List<CartItem> items = new ArrayList<>();

@Column(name = "subtotal", precision = 19, scale = 2)
private BigDecimal subtotal = BigDecimal.ZERO;

@Column(name = "discount", precision = 19, scale = 2)
private BigDecimal discount = BigDecimal.ZERO;

@Column(name = "total", precision = 19, scale = 2)
private BigDecimal total = BigDecimal.ZERO;

@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (status == null) {
status = CartStatus.ACTIVE;
}
}

@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}

public void addItem(CartItem item) {
items.add(item);
item.setCart(this);
}

public void removeItem(CartItem item) {
items.remove(item);
item.setCart(null);
}

public boolean isEmpty() {
return items == null || items.isEmpty();
}

public int getItemCount() {
return items == null ? 0 : items.size();
}

public int getTotalQuantity() {
return items == null ? 0 : items.stream()
.mapToInt(CartItem::getQuantity)
.sum();
}
}

@Getter
public enum CartStatus {
ACTIVE("Active"),
SAVED("Saved for Later"),
CHECKOUT("In Checkout"),
ABANDONED("Abandoned"),
CONVERTED("Converted to Order");

private final String displayName;

CartStatus(String displayName) {
this.displayName = displayName;
}
}

Service Implementation: CartService

What Is the Cart Service?

The CartService encapsulates all business logic for cart operations including item management, pricing calculations, session handling, and checkout preparation.

Why Is It Needed?

  • Business Logic: Centralizes cart rules and workflows
  • Transaction Management: Ensures data consistency
  • Pricing Integration: Coordinates with pricing engine
  • Session Management: Handles cart-session association
  • Event Publishing: Notifies other services of cart changes

How/When Is It Used?

  • API Calls: Invoked by Controller for all cart operations
  • Item Management: Add, update, remove cart items
  • Checkout: Prepare cart data for order creation
  • Recovery: Retrieve abandoned carts for marketing
  • Cleanup: Scheduled tasks to remove expired carts

Service Operation Flow

Code Example (Java)

// filepath: /sureink-cloud-msa/src/main/java/com/sureink/cart/service/CartService.java
@Service
@Transactional
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
@Slf4j
public class CartService {

private final CartRepository cartRepository;
private final CartItemRepository cartItemRepository;
private final ProductService productService;
private final PricingService pricingService;
private final CartEventPublisher eventPublisher;

@Value("${cart.items.maxItemsPerCart:100}")
private int maxItemsPerCart;

@Value("${cart.items.maxQuantityPerItem:999}")
private int maxQuantityPerItem;

/**
* Add item to cart
*/
public CartDto addItem(Long cartId, Long productId, int quantity) {
log.info("Adding item to cart: cartId={}, productId={}, quantity={}",
cartId, productId, quantity);

Cart cart = cartRepository.findById(cartId)
.orElseThrow(() -> new CartNotFoundException(cartId));

if (cart.getItemCount() >= maxItemsPerCart) {
throw new CartLimitExceededException("Cart cannot exceed " + maxItemsPerCart + " items");
}

if (quantity > maxQuantityPerItem) {
throw new InvalidQuantityException("Quantity cannot exceed " + maxQuantityPerItem);
}

// Check if item already exists in cart
Optional<CartItem> existingItem = cart.getItems().stream()
.filter(item -> item.getProduct().getId().equals(productId))
.findFirst();

if (existingItem.isPresent()) {
// Update quantity of existing item
CartItem item = existingItem.get();
int newQuantity = item.getQuantity() + quantity;
if (newQuantity > maxQuantityPerItem) {
throw new InvalidQuantityException("Total quantity would exceed " + maxQuantityPerItem);
}
item.setQuantity(newQuantity);
updateItemPricing(item);
} else {
// Add new item
Product product = productService.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));

CartItem newItem = new CartItem();
newItem.setProduct(product);
newItem.setQuantity(quantity);
updateItemPricing(newItem);

cart.addItem(newItem);
}

recalculateCartTotals(cart);
Cart savedCart = cartRepository.save(cart);

eventPublisher.publishCartItemAddedEvent(savedCart, productId, quantity);

log.info("Item added to cart successfully: cartId={}", cartId);
return CartMapper.toDto(savedCart);
}

/**
* Update item quantity
*/
public CartDto updateItemQuantity(Long cartId, Long itemId, int quantity) {
log.info("Updating cart item quantity: cartId={}, itemId={}, quantity={}",
cartId, itemId, quantity);

Cart cart = cartRepository.findById(cartId)
.orElseThrow(() -> new CartNotFoundException(cartId));

CartItem item = cart.getItems().stream()
.filter(i -> i.getId().equals(itemId))
.findFirst()
.orElseThrow(() -> new CartItemNotFoundException(itemId));

if (quantity <= 0) {
cart.removeItem(item);
cartItemRepository.delete(item);
} else {
if (quantity > maxQuantityPerItem) {
throw new InvalidQuantityException("Quantity cannot exceed " + maxQuantityPerItem);
}
item.setQuantity(quantity);
updateItemPricing(item);
}

recalculateCartTotals(cart);
Cart savedCart = cartRepository.save(cart);

eventPublisher.publishCartItemUpdatedEvent(savedCart, itemId, quantity);

return CartMapper.toDto(savedCart);
}

/**
* Remove item from cart
*/
public CartDto removeItem(Long cartId, Long itemId) {
log.info("Removing item from cart: cartId={}, itemId={}", cartId, itemId);

Cart cart = cartRepository.findById(cartId)
.orElseThrow(() -> new CartNotFoundException(cartId));

CartItem item = cart.getItems().stream()
.filter(i -> i.getId().equals(itemId))
.findFirst()
.orElseThrow(() -> new CartItemNotFoundException(itemId));

cart.removeItem(item);
cartItemRepository.delete(item);

recalculateCartTotals(cart);
Cart savedCart = cartRepository.save(cart);

eventPublisher.publishCartItemRemovedEvent(savedCart, itemId);

return CartMapper.toDto(savedCart);
}

/**
* Clear all items from cart
*/
public CartDto clearCart(Long cartId) {
log.info("Clearing cart: cartId={}", cartId);

Cart cart = cartRepository.findById(cartId)
.orElseThrow(() -> new CartNotFoundException(cartId));

cart.getItems().clear();
cart.setSubtotal(BigDecimal.ZERO);
cart.setDiscount(BigDecimal.ZERO);
cart.setTotal(BigDecimal.ZERO);

Cart savedCart = cartRepository.save(cart);

eventPublisher.publishCartClearedEvent(savedCart);

return CartMapper.toDto(savedCart);
}

// Helper methods

private void updateItemPricing(CartItem item) {
BigDecimal unitPrice = pricingService.getPrice(item.getProduct().getId());
item.setUnitPrice(unitPrice);
item.setLineTotal(unitPrice.multiply(BigDecimal.valueOf(item.getQuantity())));
}

private void recalculateCartTotals(Cart cart) {
BigDecimal subtotal = cart.getItems().stream()
.map(CartItem::getLineTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);

cart.setSubtotal(subtotal);

// Apply discounts if configured
BigDecimal discount = pricingService.calculateDiscount(cart);
cart.setDiscount(discount);

cart.setTotal(subtotal.subtract(discount));
}
}

Usage Examples

Creating and Managing a Cart

What Is Cart Creation?

The process of initializing a new shopping cart for a user session or customer.

Code Example (Java)

// Create new cart for customer
Cart cart = new Cart();
cart.setCustomer(customer);
cart.setSessionId(sessionId);
cart.setStatus(CartStatus.ACTIVE);
cart.setExpiresAt(LocalDateTime.now().plusDays(30));

Cart savedCart = cartRepository.save(cart);

Adding Items to Cart

What Is Item Addition?

Adding products to the cart with specified quantities and automatic pricing.

Code Example (Java)

// Add item to cart via service
CartDto cart = cartService.addItem(
cartId,
productId,
quantity
);

// Add product plan to cart
CartDto cartWithPlan = cartService.addProductPlan(
cartId,
productPlanId,
billingPeriod
);

Updating Cart Items

What Is Item Update?

Modifying the quantity of items already in the cart.

Code Example (Java)

// Update item quantity
CartDto updatedCart = cartService.updateItemQuantity(
cartId,
cartItemId,
newQuantity
);

// Remove item from cart
CartDto cartAfterRemoval = cartService.removeItem(
cartId,
cartItemId
);

Verification

Cart Service Tests

What Are Cart Service Tests?

Unit and integration tests to verify cart operations and business rules.

Code Example (Java)

// filepath: /sureink-cloud-msa/src/test/java/com/sureink/cart/service/CartServiceTest.java
@SpringBootTest
@Transactional
class CartServiceTest {

@Autowired
private CartService cartService;

@Autowired
private CartRepository cartRepository;

@MockBean
private PricingService pricingService;

@Test
void shouldAddItemToCart() {
// Given
Cart cart = createTestCart();
Long productId = 100L;
int quantity = 2;

when(pricingService.getPrice(productId))
.thenReturn(new BigDecimal("29.99"));

// When
CartDto result = cartService.addItem(cart.getId(), productId, quantity);

// Then
assertThat(result.getItemCount()).isEqualTo(1);
assertThat(result.getTotalQuantity()).isEqualTo(quantity);
verify(pricingService).getPrice(productId);
}

@Test
void shouldUpdateItemQuantity() {
// Given
Cart cart = createCartWithItems();
CartItem item = cart.getItems().get(0);
int newQuantity = 5;

when(pricingService.getPrice(any()))
.thenReturn(new BigDecimal("29.99"));

// When
CartDto result = cartService.updateItemQuantity(
cart.getId(),
item.getId(),
newQuantity
);

// Then
assertThat(result.getItems().get(0).getQuantity()).isEqualTo(newQuantity);
}

@Test
void shouldEnforceMaxItemsLimit() {
// Given
Cart cart = createTestCart();

// When/Then
assertThatThrownBy(() -> {
for (int i = 0; i < 101; i++) {
cartService.addItem(cart.getId(), 100L + i, 1);
}
}).isInstanceOf(CartLimitExceededException.class);
}

@Test
void shouldCalculateTotalsCorrectly() {
// Given
Cart cart = createTestCart();

when(pricingService.getPrice(any()))
.thenReturn(new BigDecimal("10.00"));
when(pricingService.calculateDiscount(any()))
.thenReturn(new BigDecimal("2.00"));

// When
cartService.addItem(cart.getId(), 100L, 3);
CartDto result = cartService.findById(cart.getId());

// Then
assertThat(result.getSubtotal()).isEqualByComparingTo("30.00");
assertThat(result.getDiscount()).isEqualByComparingTo("2.00");
assertThat(result.getTotal()).isEqualByComparingTo("28.00");
}
}

Related Topics: