Implement the shopping cart
For the complete documentation index see: llms.txt
All documentation pages available in markdown.
In this step you implement the shopping-cart methods. The cart is stored as a single record per user, with the cart contents in a nested map. You implement getCart first, then the three sub-steps of addToCart: load the cart and its metadata, update an existing item with check-and-set semantics, and insert a brand-new cart when one does not exist.
The previous step left the home page listing products and the search filters working. The cart code returns hard-coded items. The cartDataSet writes to the shopping_carts set, and a cartMapper converts Cart objects to and from Aerospike records. Every code change in this step lives in spring-server/src/main/java/com/aerospikeworkshop/service/KeyValueServiceNewClient.java.
Implement getCart
The getCart(userId) method returns the user’s cart, or an empty Cart if the user has none yet. The method body returns a hard-coded cart with one item.
-
Find
getCartinKeyValueServiceNewClient.java. -
Delete the entire hard-coded
return new Cart(Map.of(...));statement, including every line it spans, then paste the following in its place. If any fragment of the old statement remains, the file no longer compiles.return session.query(cartDataSet.id(userId)).execute().getFirst(cartMapper).orElseGet(() -> new Cart());The chain looks the same as the
getProductchain from the earlier step, with one addition:orElseGet(() -> new Cart())returns an empty cart when the lookup returns no record.
Confirm getCart works
-
Stop the Spring Boot application with
Ctrl+C, then rebuild and rerun it from thespring-serverdirectory.Terminal window cd spring-servermvn clean package -DskipTestsmvn spring-boot:run -Dspring-boot.run.profiles=new-clientWait for
BUILD SUCCESSfrom the first command before you start the second. Run them one at a time. -
Reload the home page and open the cart icon.
The cart is empty (no hard-coded items). The cart still cannot be modified because
addToCartis not yet implemented.
Understand the cart record shape
Before you implement addToCart, look at the on-disk shape of a cart record. The Javadoc on addToCart defines the desired structure:
{ "items": { "15943": { "brandName": "Turtle", "image": "http://...3f46677767988641d7a_images.jpg", "name": "Turtle Men Leather Black Wallets", "price": 995, "productId": "15943", "quantity": 1, "userId": "user_uv4ytwx6h" }, "41213": { "brandName": "Lotto", "image": "http://...f93ee70287eece69ac_images.jpg", "name": "Lotto Men Black Flip Flops", "price": 219, "productId": "41213", "quantity": 4, "userId": "user_uv4ytwx6h" } }}A cart record has one bin named items (also exposed as the ITEMS_BIN constant). The items bin holds a map keyed by productId. Each value is itself a map with the per-item fields brandName, image, name, price, productId, quantity, and userId.
To increase the quantity of an existing item, you update the quantity entry of one inner map without touching anything else. To add the first item to a user’s first cart, you insert a record whose items bin contains a single inner map.
Implement addToCart, sub-step 7a: load the cart with metadata
You read the cart and its generation before any update so that you can use a check-and-set pattern. The update succeeds only if the record’s generation has not changed since you read it.
The addToCart method is the second-to-last method in KeyValueServiceNewClient.java. It contains three numbered TODOs (STEP 7a, STEP 7b, STEP 7c) that you fill in across the next three sub-steps.
-
In
addToCart, replace the entireOptional<ObjectWithMetadata<Cart>> cartAndMetadata = ...;statement under// TODO: STEP 7a(a synchronousgetCart(userId)call wrapped in a fakeRecord) with a real point read that returns the cart and its current generation.The full statement to delete (including the placeholder
Recordconstructor that you patched earlier in the tutorial) is:Optional<ObjectWithMetadata<Cart>> cartAndMetadata =Optional.of(new ObjectWithMetadata<Cart>(getCart(userId), new Record(1, 1)));Replace it with:
Optional<ObjectWithMetadata<Cart>> cartAndMetadata =session.query(key).execute().getFirstWithMetadata(cartMapper);Two things differ from a normal point read:
getFirstWithMetadatareturns anObjectWithMetadata<Cart>instead of a plainCart. The wrapper exposes the record’s generation, last-update time, and other metadata along with the mapped object. (If you were not using object mapping, you would read the underlyingRecorddirectly: records always carry their metadata, so the wrapper type is not needed.)- The result is wrapped in an
Optionalbecause the user may not have a cart yet.
Implement addToCart, sub-step 7b: update an existing item with check-and-set
Sub-step 7b runs only when the cart already exists and already contains the same product. The job is to add the requested quantity to the existing quantity map entry.
-
In
addToCart, locate the first lambda passed tocart.findItem(productId).ifPresentOrElse(...), the branch that begins:.ifPresentOrElse(item -> {// The item exists in the record, just update the quantityitem.setQuantity(item.getQuantity() + quantity);This is the branch that runs when the product is already in the cart. Directly under the
// TODO: STEP 7bcomment, after theitem.setQuantity(...)line and before the closing},, add the SDK call:session.update(key).bin(ITEMS_BIN).onMapKey(productId).onMapKey("quantity").add(quantity).ensureGenerationIs(cartWithMetadata.getGeneration()).execute();Read the chain top to bottom:
update(key)selects the cart record. The verb fails if the record does not exist, which guards against a race where the cart is deleted between your read and your write..bin(ITEMS_BIN).onMapKey(productId).onMapKey("quantity").add(quantity)walks the document hierarchy: start at theitemsbin, drill into the inner map keyed byproductId, drill again into the entry keyed byquantity, and add the requested amount. The SDK builds a single nested map operation so the change is atomic on the server..ensureGenerationIs(cartWithMetadata.getGeneration())enforces check-and-set. If another writer changed the cart between your read and your write, the generation no longer matches and the server throwsGenerationException, a child ofAerospikeException. The surroundingaddToCartcode already catchesGenerationExceptionand retries the entire flow.
The cart update would technically be safe without check-and-set, because that single nested map operation is atomic on the server. The pattern is shown here because it is common in business code, where you may need to recompute a derived value before writing, and because it is the simplest way to surface conflicts to the caller as a retryable error.
Implement addToCart, sub-step 7c: create a new cart
Sub-step 7c runs when the user has no cart yet. The sample helper code already populated a CartItem named newItem; you insert a brand-new record whose items bin contains that one item.
-
In
addToCart, locate the outer.orElseGet(() -> { ... })lambda (the branch that runs whencartAndMetadatais empty). It already contains:Cart cart = new Cart();CartItem newItem = new CartItem(userId, quantity, image, product);cart.add(newItem);// TODO: STEP 7c: ...return cart;Directly under the
// TODO: STEP 7ccomment, before thereturn cart;line, add the SDK call:session.insert(key).bin(ITEMS_BIN).onMapKey(productId).setTo(newItem, cartItemMapper).execute();The chain uses
insertrather thanupdatebecause the previous read returned no record. If a record was created between your read and your write,insertfails withAerospikeException, which the surrounding loop retries through the same check-and-set flow as sub-step 7b. The new record’sitemsbin is built bysetTo(newItem, cartItemMapper), which uses a separateRecordMapperthat knows how to convert aCartItemto a map of bin-style entries.
Confirm addToCart works end to end
-
Stop the Spring Boot application with
Ctrl+C, then rebuild and rerun it.Terminal window cd spring-servermvn clean package -DskipTestsmvn spring-boot:run -Dspring-boot.run.profiles=new-client -
Reload the home page, select a few products, and add them to your cart.
-
Open the cart in the browser and confirm the items, quantities, and total are correct.
-
In Voyager, refresh the namespace and select the
shopping_cartsset.The set contains one cart record. Expand it to confirm the nested structure: an
itemsbin with one entry per product you added.
The record key is a randomly generated user identifier such as
user_uv4ytwx6h. In production, derive the user identifier from the authenticated session.
Edit cart data with Voyager
Voyager is not only a data browser. The </> icon next to each level of the record opens an in-place JSON editor.
-
In Voyager, select the
</>icon next to one of the items in the cart. -
Change the
quantityvalue to a new number, then click the green checkmark icon next to the field to save.Voyager writes the change back to the cluster. To discard an edit instead, click the red
xicon. -
Reload the cart in the browser to confirm that the new quantity appears.
Editing data in place skips your application’s validation logic. Use Voyager’s editor for development, debugging, and quick fixes only. Never edit production data this way without an audit trail.
Outcomes
You now have a fully working retail application backed by Aerospike. Specifically:
storeProduct,getProduct,query,advancedSearch,getCart, andaddToCartare all implemented against the live cluster.- The
addToCartflow uses a check-and-set update against a nested map document to make concurrent updates safe. - You can browse, filter, and edit the underlying data in Voyager.
If you want to compare your implementation to a reference, look at the KeyValueServiceNewClientAnswers class in the sample repository.
Clean up
Stop the Spring Boot application and the Aerospike Database container so they do not occupy ports 8080 and 3000 to 3003 after you finish.
-
In the terminal that is running the Spring Boot application, press
Ctrl+Cto stop the server. -
From the
aerospike-client-sdk-workshoprepository root, stop and remove the Aerospike container.Terminal window docker compose down[+] Running 2/2✔ Container aerospike-workshop Removed✔ Network aerospike-client-sdk-workshop_default RemovedExample response This command stops and removes the
aerospike-workshopcontainer and its network. The named volumeaerospike-client-sdk-workshop_aerospike-dataremains, so the records you wrote in this tutorial are still present the next time you rundocker compose up -d. -
(Optional) Delete the data volume to start fresh next time.
Terminal window docker compose down --volumesAdding
--volumesremoves all records from the local database. Run this command only when you are sure you do not need the tutorial data again. -
(Optional) Disconnect Voyager from the local cluster, or remove the cluster entry, so it does not try to reconnect when you next launch Voyager.