Cart Storage Strategies
- Database Storage: Traditional relational databases introduce performance bottlenecks under heavy read/write loads.
- Client-Side Storage: Browsers offer
localStoragefor persistent key-value data without expiration, andsessionStoragefor data cleared upon tab closure. However, these lack server-side synchronization. - Redis Cache: High-performance in-memory storage. Enabling AOF (Append Only File) persistence mitigates data loss during server restarts.
- Redis with Database Sync: Combines Redis speed with database durability. Since cart operations are scoped to individual users, high-concurrency conflicts on the same data record are rare, simplifying consistency management.
Data Architecture in Redis
A shopping cart consists of multiple items. To model this efficiently, a nested Map structure Map<String, Map<String, String>> is ideal.
- The outer Map key represents the user identifier.
- The inner Map key represents the product identifier, with the value containing the item details.
This maps perfectly to the Redis Hash data type, where the Hash key serves as the user identifier, the Hash fields represent product identifiers, and the values store the item details as serialized JSON strings.
API Endpoint
@RestController
@RequestMapping("/api/carts")
public class ShoppingCartController {
@Autowired
private RedisShoppingCartManager cartManager;
@PostMapping("/items")
public ApiResponse addItemToCart(@RequestBody AddItemCommand command) {
cartManager.addItem(command);
return ApiResponse.success();
}
}Service Logic
The core logic interacts with Redis Hash operations to manage cart entries:
- Retrieve the current user identifier from the request context.
- Construct the Redis Hash key specific to the user.
- Bind the Hash operations for that specific cart key.
- Fetch the existing entry for the requested product identifier.
- If the product is absent, query the product service, construct a new cart item object, serialize it to JSON, and persist it in the Redis Hash.
- If the product exists, deserialize the JSON, increment the item quantity, serialize it back, and update the Redis Hash.
@Service
@Slf4j
public class RedisShoppingCartManager {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductGateway productGateway;
public void addItem(AddItemCommand command) {
Long itemIdentifier = command.getItemIdentifier();
int addedQuantity = command.getQuantity();
BoundHashOperations<String, Object, Object> userCartHash = retrieveCartOperations();
String serializedItem = (String) userCartHash.get(String.valueOf(itemIdentifier));
if (serializedItem == null || serializedItem.isEmpty()) {
ProductInfo info = productGateway.fetchDetails(itemIdentifier);
if (info == null) {
throw new BusinessException(ErrorCode.ITEM_NOT_FOUND);
}
CartProductView newEntry = new CartProductView();
newEntry.setItemIdentifier(itemIdentifier);
newEntry.setTitle(info.getTitle());
newEntry.setImageUrl(info.getImageUrl());
newEntry.setPrice(info.getPrice());
newEntry.setQuantity(addedQuantity);
userCartHash.put(String.valueOf(itemIdentifier), JsonUtils.toJson(newEntry));
} else {
CartProductView existingEntry = JsonUtils.fromJson(serializedItem, CartProductView.class);
existingEntry.incrementQuantity(addedQuantity);
userCartHash.put(String.valueOf(itemIdentifier), JsonUtils.toJson(existingEntry));
}
}
private BoundHashOperations<String, Object, Object> retrieveCartOperations() {
String cartKey = buildCartRedisKey();
return redisTemplate.boundHashOps(cartKey);
}
private String buildCartRedisKey() {
AuthenticatedUser currentUser = RequestContext.getCurrentUser();
return String.format("cart:user:%s", currentUser.getId());
}
}