Skip to content

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.

  1. Find get_cart in key_value_service_new_client.py.

  2. Delete the entire hard-coded cart block inside the try (the product = await self.get_product(...) lines through the return 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 empty Cart() instead of None.

Confirm get_cart works

  1. Stop the FastAPI application with Ctrl+C, then restart it from the python-server directory.

    Terminal window
    cd python-server
    AEROSPIKE_CLIENT_PROFILE=new-client AEROSPIKE_PORT=3000 \
    uvicorn aerospikeworkshop.main:app --host 0.0.0.0 --port 8080
  2. 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_cart is 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.

  1. In add_to_cart, delete the placeholder lines inside the try block under TODO: STEP 7:

    cart = await self.get_cart(user_id)
    generation = 1
    if cart.find_item(product_id) is not None:
    ...
    else:
    ...
    result_cart = cart
  2. 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.generation

    Two things differ from a normal point read:

    • result.record.generation exposes the record’s generation for check-and-set updates.
    • The if result is not None and result.is_ok branch is the positive form of the get_product guard. 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.

  1. Inside the if result is not None and result.is_ok ... branch from sub-step 7a, add logic for an existing cart. After you set generation, add:

    existing = cart.find_item(product_id)
    if existing is not None:
    existing.quantity += quantity
    await (
    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 = cart

    When the item already exists, existing.quantity += quantity updates the in-memory Cart so result_cart reflects 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 the items bin, drill into the inner map keyed by productId, drill again into the entry keyed by quantity, 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 raises GenerationError.

    Sub-steps 7a through 7c replace the placeholder inside an existing retry loop in add_to_cart. The method already imports GenerationError and wraps the TODO block like this—leave that wrapper in place:

    result_cart = None
    while result_cart is None:
    try:
    # sub-steps 7a–7c go here
    ...
    result_cart = cart
    except GenerationError:
    logger.info("Lost race condition when adding product %s", product_id)

    On GenerationError, result_cart stays None, 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).

  1. Add an else branch at the same indentation as the if result is not None and result.is_ok ... block from sub-step 7a. Do not nest it inside the inner if existing is not None / else pair 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 = cart

    After sub-steps 7a through 7c, the inner try block (inside the while loop 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.generation
    existing = cart.find_item(product_id)
    if existing is not None:
    ...
    else:
    ...
    result_cart = cart
    else:
    ...
    result_cart = cart

    The chain uses insert rather than update because the previous read returned no record. If a record was created between your read and your write, insert fails and the surrounding loop retries through the same check-and-set flow as sub-step 7b.

Confirm add_to_cart works end to end

  1. Stop the FastAPI application with Ctrl+C, then restart it.

    Terminal window
    cd python-server
    AEROSPIKE_CLIENT_PROFILE=new-client AEROSPIKE_PORT=3000 \
    uvicorn aerospikeworkshop.main:app --host 0.0.0.0 --port 8080
  2. Reload the home page, select a few products, and add them to your cart.

  3. Open the cart in the browser and confirm the items, quantities, and total are correct.

  4. In Voyager, refresh the namespace and select the shopping_carts set.

    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 items bin with one entry per product you added.

    Voyager showing the shopping_carts set with one record whose items bin contains the products added from the UI

    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.

  1. In Voyager, select the </> icon next to one of the items in the cart.

  2. Change the quantity value 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 x icon.

  3. 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.