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 get_cart first, then the three sub-steps of add_to_cart: 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 cart_dataset writes to the shopping_carts set. Every code change in this step lives in python-server/src/aerospikeworkshop/services/key_value_service_new_client.py.
Implement get_cart
The get_cart(user_id) 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 when a product lookup succeeds.
-
Find
get_cartinkey_value_service_new_client.py. -
Delete the entire hard-coded cart block inside the
try(theproduct = await self.get_product(...)lines through thereturn Cart(items={...})statement), then paste the following in its place:session = self._require_session()stream = await session.query(self.cart_dataset.id(user_id)).execute()result = await stream.first()stream.close()if result is None or not result.is_ok:return Cart()return Cart.from_bins(result.record.bins)The query chain and result guard match
get_product. When the guard fails, return an emptyCart()instead ofNone.
Confirm get_cart works
-
Stop the FastAPI application with
Ctrl+C, then restart it from thepython-serverdirectory.Terminal window cd python-serverAEROSPIKE_CLIENT_PROFILE=new-client AEROSPIKE_PORT=3000 \uvicorn aerospikeworkshop.main:app --host 0.0.0.0 --port 8080 -
Reload the home page and open the cart icon.
The cart is empty (no hard-coded items). The cart still cannot be modified because
add_to_cartis not yet implemented.
Understand the cart record shape
Before you implement add_to_cart, look at the on-disk shape of a cart record. 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.
{ "items": { "33396": { "brandName": "Baggit", "image": "http://...a42e87b57a01a1618cbadec725d43aea_images.jpg", "name": "Baggit Women Kites Jimmy Purple Wallet", "price": 575, "productId": "33396", "quantity": 1, "userId": "user_uv4ytwx6h" }, "7710": { "brandName": "Puma", "image": "http://...26e497d5dcfd18e65efa845247e07889_images.jpg", "name": "Puma Men's Bluebird Blue Yellow Shoe", "price": 5999, "productId": "7710", "quantity": 4, "userId": "user_uv4ytwx6h" } }}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 add_to_cart, 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. Aerospike increments a counter called the record generation on every write. The update succeeds only if the record’s generation has not changed since you read it, which prevents lost updates when two requests modify the same cart at once.
The add_to_cart method contains a single TODO: STEP 7 block that you fill in across the next three sub-steps.
-
In
add_to_cart, delete the placeholder lines inside thetryblock underTODO: STEP 7:cart = await self.get_cart(user_id)generation = 1if cart.find_item(product_id) is not None:...else:...result_cart = cart -
Replace them with a point read that returns the cart and its current generation:
stream = await session.query(key).execute()result = await stream.first()stream.close()if result is not None and result.is_ok:cart = Cart.from_bins(result.record.bins)generation = result.record.generationTwo things differ from a normal point read:
result.record.generationexposes the record’s generation for check-and-set updates.- The
if result is not None and result.is_okbranch is the positive form of theget_productguard. It runs only when a cart record exists; sub-step 7c handles the missing-cart case.
Implement add_to_cart, sub-step 7b: update an existing item with check-and-set
Sub-step 7b runs only when the cart already exists. If the product is already in the cart, increment its quantity. If the cart exists but the product is new, add a new map entry.
-
Inside the
if result is not None and result.is_ok ...branch from sub-step 7a, add logic for an existing cart. After you setgeneration, add:existing = cart.find_item(product_id)if existing is not None:existing.quantity += quantityawait (session.update(key).bin(ITEMS_BIN).on_map_key(product_id).on_map_key("quantity").add(quantity).ensure_generation_is(generation).execute())else:new_item = CartItem.from_product(user_id, quantity, image, product)cart.add(new_item)await (session.update(key).bin(ITEMS_BIN).on_map_key(product_id).set_to(new_item.to_bins()).ensure_generation_is(generation).execute())result_cart = cartWhen the item already exists,
existing.quantity += quantityupdates the in-memoryCartsoresult_cartreflects the new total without a second read. The.add(quantity)call is the server-side update that persists the same increment atomically—they are not two separate additions.Read the update chain for an existing item 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).on_map_key(product_id).on_map_key("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..ensure_generation_is(generation)enforces check-and-set. If another writer changed the cart between your read and your write, the generation no longer matches and the server raisesGenerationError.
Sub-steps 7a through 7c replace the placeholder inside an existing retry loop in
add_to_cart. The method already importsGenerationErrorand wraps the TODO block like this—leave that wrapper in place:result_cart = Nonewhile result_cart is None:try:# sub-steps 7a–7c go here...result_cart = cartexcept GenerationError:logger.info("Lost race condition when adding product %s", product_id)On
GenerationError,result_cartstaysNone, so the loop re-reads the cart and retries from the top.
Implement add_to_cart, sub-step 7c: create a new cart
Sub-step 7c runs when the user has no cart yet (result is empty or the record is missing).
-
Add an
elsebranch at the same indentation as theif result is not None and result.is_ok ...block from sub-step 7a. Do not nest it inside the innerif existing is not None/elsepair from sub-step 7b:else:cart = Cart()new_item = CartItem.from_product(user_id, quantity, image, product)cart.add(new_item)await (session.insert(key).bin(ITEMS_BIN).on_map_key(product_id).set_to(new_item.to_bins()).execute())result_cart = cartAfter sub-steps 7a through 7c, the inner
tryblock (inside thewhileloop from sub-step 7b) has this shape:stream = await session.query(key).execute()result = await stream.first()stream.close()if result is not None and result.is_ok:cart = Cart.from_bins(result.record.bins)generation = result.record.generationexisting = cart.find_item(product_id)if existing is not None:...else:...result_cart = cartelse:...result_cart = cartThe chain uses
insertrather thanupdatebecause the previous read returned no record. If a record was created between your read and your write,insertfails and the surrounding loop retries through the same check-and-set flow as sub-step 7b.
Confirm add_to_cart works end to end
-
Stop the FastAPI application with
Ctrl+C, then restart it.Terminal window cd python-serverAEROSPIKE_CLIENT_PROFILE=new-client AEROSPIKE_PORT=3000 \uvicorn aerospikeworkshop.main:app --host 0.0.0.0 --port 8080 -
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 for each browser session you added items from. Expand the most recent one 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
xicon. -
Reload the cart in the browser to confirm that the new quantity appears.
Continue to Wrap up to review what you built and stop the local environment.