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
| Stage | Description | Linked Objects | Allowed Transitions | Business Rules |
|---|---|---|---|---|
| DRAFT | Initial cart creation state. Items can be added, removed, or modified. No licenses created yet. | Cart, CartItems | PLACED, CANCELED | - Cart must have at least one product to transition to PLACED - Products can be freely modified - No licenses or subscriptions exist |
| PLACED | Order 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_PROGRESS | Payment 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_ACCEPT | Manual 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 |
| COMPLETED | Payment 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 |
| DONE | All 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 |
| CANCELED | Cart 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 Stage | Payment Status | Description |
|---|---|---|
| DRAFT | N/A | No payment initiated |
| PLACED | UNPAID | Trial licenses created, awaiting payment |
| PAYMENT_IN_PROGRESS | IN_PROGRESS | Payment processing |
| AWAITING_ACCEPT | UNPAID | Manual approval, payment pending |
| COMPLETED | PAID | Payment successful, licenses activated |
| DONE | PAID | All transactions complete |
| CANCELED | FAILED/UNKNOWN | Payment 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:
- Cart Dependencies - Core libraries for cart management and session handling
- 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: