---
title: "Implement the shopping cart"
description: "Implement a shopping cart in Java using Aerospike's nested map operations, CAS updates, and Voyager data editing."
---

# Implement the shopping cart

> For the complete documentation index see: [llms.txt](https://aerospike.com/docs/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.

1.  Find `getCart` in `KeyValueServiceNewClient.java`.
    
2.  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.
    
    ```java
    return session.query(cartDataSet.id(userId))
    
        .execute()
    
        .getFirst(cartMapper)
    
        .orElseGet(() -> new Cart());
    ```
    
    The chain looks the same as the `getProduct` chain from the earlier step, with one addition: `orElseGet(() -> new Cart())` returns an empty cart when the lookup returns no record.
    

### Confirm getCart works

1.  Stop the Spring Boot application with `Ctrl+C`, then rebuild and rerun it from the `spring-server` directory.
    
    Terminal window
    
    ```shell
    cd spring-server
    
    mvn clean package -DskipTests
    
    mvn spring-boot:run -Dspring-boot.run.profiles=new-client
    ```
    
    Wait for `BUILD SUCCESS` from the first command before you start the second. Run them one at a time.
    
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 `addToCart` is 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:

```json
{

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

1.  In `addToCart`, replace the entire `Optional<ObjectWithMetadata<Cart>> cartAndMetadata = ...;` statement under `// TODO: STEP 7a` (a synchronous `getCart(userId)` call wrapped in a fake `Record`) with a real point read that returns the cart and its current generation.
    
    The full statement to delete (including the placeholder `Record` constructor that you patched earlier in the tutorial) is:
    
    ```java
    Optional<ObjectWithMetadata<Cart>> cartAndMetadata =
    
        Optional.of(new ObjectWithMetadata<Cart>(getCart(userId), new Record(1, 1)));
    ```
    
    Replace it with:
    
    ```java
    Optional<ObjectWithMetadata<Cart>> cartAndMetadata =
    
        session.query(key)
    
            .execute()
    
            .getFirstWithMetadata(cartMapper);
    ```
    
    Two things differ from a normal point read:
    
    -   `getFirstWithMetadata` returns an `ObjectWithMetadata<Cart>` instead of a plain `Cart`. 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 underlying `Record` directly: records always carry their metadata, so the wrapper type is not needed.)
    -   The result is wrapped in an `Optional` because 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.

1.  In `addToCart`, locate the first lambda passed to `cart.findItem(productId).ifPresentOrElse(...)`, the branch that begins:
    
    ```java
    .ifPresentOrElse(item -> {
    
        // The item exists in the record, just update the quantity
    
        item.setQuantity(item.getQuantity() + quantity);
    ```
    
    This is the branch that runs when the product is already in the cart. Directly under the `// TODO: STEP 7b` comment, after the `item.setQuantity(...)` line and before the closing `},`, add the SDK call:
    
    ```java
    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 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.
    -   `.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 throws `GenerationException`, a child of `AerospikeException`. The surrounding `addToCart` code already catches `GenerationException` and 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.

1.  In `addToCart`, locate the outer `.orElseGet(() -> { ... })` lambda (the branch that runs when `cartAndMetadata` is empty). It already contains:
    
    ```java
    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 7c` comment, before the `return cart;` line, add the SDK call:
    
    ```java
    session.insert(key)
    
        .bin(ITEMS_BIN).onMapKey(productId).setTo(newItem, cartItemMapper)
    
        .execute();
    ```
    
    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 with `AerospikeException`, which the surrounding loop retries through the same check-and-set flow as sub-step 7b. The new record’s `items` bin is built by `setTo(newItem, cartItemMapper)`, which uses a separate `RecordMapper` that knows how to convert a `CartItem` to a map of bin-style entries.
    

### Confirm addToCart works end to end

1.  Stop the Spring Boot application with `Ctrl+C`, then rebuild and rerun it.
    
    Terminal window
    
    ```shell
    cd spring-server
    
    mvn clean package -DskipTests
    
    mvn spring-boot:run -Dspring-boot.run.profiles=new-client
    ```
    
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. Expand it 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](https://aerospike.com/docs/_astro/voyager-cart-record.C9YS-hSo_Z1D8Brs.png)
    
    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 red `x` icon.
    
3.  Reload the cart in the browser to confirm that the new quantity appears.
    

::: caution
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`, and `addToCart` are all implemented against the live cluster.
-   The `addToCart` flow 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.

1.  In the terminal that is running the Spring Boot application, press `Ctrl+C` to stop the server.
    
2.  From the `aerospike-client-sdk-workshop` repository root, stop and remove the Aerospike container.
    
    Terminal window
    
    ```shell
    docker compose down
    ```
    
    ```plaintext
    [+] Running 2/2
    
     ✔ Container aerospike-workshop                  Removed
    
     ✔ Network aerospike-client-sdk-workshop_default Removed
    ```
    
    Example response
    
    This command stops and removes the `aerospike-workshop` container and its network. The named volume `aerospike-client-sdk-workshop_aerospike-data` remains, so the records you wrote in this tutorial are still present the next time you run `docker compose up -d`.
    
3.  (Optional) Delete the data volume to start fresh next time.
    
    Terminal window
    
    ```shell
    docker compose down --volumes
    ```
    
    ::: caution
    Adding `--volumes` removes all records from the local database. Run this command only when you are sure you do not need the tutorial data again.
    :::
    
4.  (Optional) Disconnect Voyager from the local cluster, or remove the cluster entry, so it does not try to reconnect when you next launch Voyager.
    

::: undefined
-   I’ve implemented getCart with a fallback to an empty Cart when none exists.
-   I’ve implemented the three parts of addToCart, including a check-and-set update.
-   I’ve added items to a cart from the application UI and confirmed the data in Voyager.
-   I’ve edited a cart record in place using Voyager’s JSON editor.
:::

Share

Congratulations on completing this tutorial! Share your achievement with the world and let everyone know about your newly acquired skills.

[](https://www.linkedin.com/feed/?shareActive&mini=true&text=I%20built%20a%20Spring%20Boot%20retail%20app%20with%20the%20Aerospike%20Java%20SDK%20and%20Aerospike%20Voyager.%0A%0AObject%20mapping%2C%20fluent%20queries%2C%20AEL%20filters%2C%20and%20check-and-set%20updates%20on%20nested%20map%20records%2C%20then%20inspected%20and%20edited%20the%20data%20live%20in%20Voyager.%0A%0A%23Aerospike%20%23Java%20%23SpringBoot%20%23Voyager%20%23DevTools%0A%0Ahttps%3A%2F%2Faerospike.com%2Fdocs%2Fdatabase%2Flearn%2Ftutorials%2Fget-started-with-aerospike-java-sdk-and-voyager "Post to LinkedIn")[](https://bsky.app/intent/compose?text=I%20built%20a%20Spring%20Boot%20retail%20app%20with%20the%20Aerospike%20Java%20SDK%20and%20Aerospike%20Voyager.%0A%0AObject%20mapping%2C%20fluent%20queries%2C%20AEL%20filters%2C%20and%20check-and-set%20updates%20on%20nested%20map%20records%2C%20then%20inspected%20and%20edited%20the%20data%20live%20in%20Voyager.%0A%0A%23Aerospike%20%23Java%20%23SpringBoot%20%23Voyager%20%23DevTools%0A%0Ahttps%3A%2F%2Faerospike.com%2Fdocs%2Fdatabase%2Flearn%2Ftutorials%2Fget-started-with-aerospike-java-sdk-and-voyager "Post to BlueSky")[](https://twitter.com/intent/tweet?text=I%20built%20a%20Spring%20Boot%20retail%20app%20with%20the%20Aerospike%20Java%20SDK%20and%20Aerospike%20Voyager.%0A%0AObject%20mapping%2C%20fluent%20queries%2C%20AEL%20filters%2C%20and%20check-and-set%20updates%20on%20nested%20map%20records%2C%20then%20inspected%20and%20edited%20the%20data%20live%20in%20Voyager.%0A%0A%23Aerospike%20%23Java%20%23SpringBoot%20%23Voyager%20%23DevTools%0A%0Ahttps%3A%2F%2Faerospike.com%2Fdocs%2Fdatabase%2Flearn%2Ftutorials%2Fget-started-with-aerospike-java-sdk-and-voyager "Post to Twitter")

::: undefined
-   Read about [Aerospike client policies](https://aerospike.com/docs/database/learn/policies) to compare the new SDK’s `Behavior` model with the legacy client’s per-call `Policy` objects.
    
-   Read about [secondary indexes](https://aerospike.com/docs/database/manage/namespace/secondary-index) to understand how the SDK’s `where()` clause picks the most selective index for your AEL expression.
    
-   Explore other Aerospike client SDKs in the [Develop overview](https://aerospike.com/docs/develop/).
:::

[Previous  
Query by secondary index](https://aerospike.com/docs/database/learn/tutorials/get-started-with-aerospike-java-sdk-and-voyager/step/2/part/1/secondary-and-advanced-queries)