Shopping Cart in Java

Learn how to create a shopping cart in Java, package it into a container, and run it on Kalix.

Before you begin

If you want to bypass writing code and jump straight to the deployment:

  1. Download the source code using the Kalix CLI: kalix quickstart download shopping-cart-java

  2. Skip to Package and deploy your service.

Generate and build the Kalix project

The Maven archetype template prompts you to specify the project’s group ID, name and version interactively. Run it using the commands shown for your OS.

In IntelliJ, you can skip the command line. Open the IDE, select File > New > Project, and click to activate Create from archetype. Use the UI to locate the archetype and fill in the blanks.

Follow these steps to generate and build your project:

  1. From a command window, run the template in a convenient location:

    Linux or macOS
    mvn archetype:generate \
      -DarchetypeGroupId=io.kalix \
      -DarchetypeArtifactId=kalix-spring-boot-archetype \
      -DarchetypeVersion=1.4.1
    Windows 10+
    mvn archetype:generate ^
      -DarchetypeGroupId=io.kalix ^
      -DarchetypeArtifactId=kalix-spring-boot-archetype ^
      -DarchetypeVersion=1.4.1
  2. Navigate to the new project directory.

  3. Open it on your preferred IDE / Editor.

Shopping Cart Service

Through our "Shopping Cart" Event Sourced Entity we expect to manage our cart, adding and removing items as we please. Being event-sourced means it will represent changes to state as a series of domain events. So let’s have a look at what kind of model we expect to store and the events our entity might generate.

Define the domain model

First, define the domain class ShoppingCart, its domain events in package shoppingcart.domain and some basic business logic.

src/main/java/shoppingcart/domain/ShoppingCart.java
package shoppingcart.domain;

import kalix.javasdk.annotations.TypeName;

import java.util.List;
import java.util.stream.Collectors;

public record ShoppingCart(String cartId, List<LineItem> items, boolean checkedOut) {


  public record LineItem(String productId, String name, int quantity) {
    public LineItem increaseQuantity(int qty) {
      return new LineItem(productId, name, quantity + qty);
    }
  }

  public ShoppingCart addItem(LineItem item) {
    var itemToAdd =
      items.stream()
        .filter(it -> it.productId.equals(item.productId))
        .findFirst()
        .map(it -> it.increaseQuantity(item.quantity))
        .orElse(item);

    return removeItem(item.productId).addAsNew(itemToAdd);
  }

  public ShoppingCart removeItem(String productId) {
    if (hasItem(productId)) {
      var updatedItems =
        items.stream()
          .filter(it -> !it.productId.equals(productId))
          .collect(Collectors.toList());

      return new ShoppingCart(cartId, updatedItems, checkedOut);
    } else {
      return this;
    }
  }


  private ShoppingCart addAsNew(LineItem item) {
    items.add(item);
    return this;
  }

  private boolean hasItem(String productId) {
    return items().stream().anyMatch(it -> it.productId.equals(productId));
  }

  public ShoppingCart checkOut() {
    return new ShoppingCart(cartId, items, true);
  }

  public sealed interface Event {

    @TypeName("item-added")
    record ItemAdded(ShoppingCart.LineItem item) implements Event {
    }

    @TypeName("item-removed")
    record ItemRemoved(String productId) implements Event {
    }

    @TypeName("checked-out")
    record CheckedOut() implements Event {
    }
  }
}

Define the external API

The Shopping Cart API is defined by the ShoppingCartEntity.

Create a class named ShoppingCartEntity in package shoppingcart.api.

src/main/java/shoppingcart/api/ShoppingCartEntity.java
import kalix.javasdk.EntityContext;
import kalix.javasdk.annotations.Id;
import kalix.javasdk.annotations.TypeId;
import kalix.javasdk.annotations.EventHandler;
import kalix.javasdk.eventsourcedentity.EventSourcedEntity;
import kalix.javasdk.eventsourcedentity.EventSourcedEntityContext;
import org.springframework.web.bind.annotation.*;
import shoppingcart.domain.ShoppingCart;
import shoppingcart.domain.ShoppingCart.Event.CheckedOut;
import shoppingcart.domain.ShoppingCart.Event.ItemAdded;
import shoppingcart.domain.ShoppingCart.Event.ItemRemoved;

import java.util.ArrayList;

@TypeId("shopping-cart") (1)
@Id("cartId") (2)
@RequestMapping("/cart/{cartId}") (3)
public class ShoppingCartEntity
  extends EventSourcedEntity<ShoppingCart, ShoppingCart.Event> { (4)

  final private String cartId;

  public ShoppingCartEntity(EventSourcedEntityContext entityContext) {
    this.cartId = entityContext.entityId();
  }

  @Override
  public ShoppingCart emptyState() { (5)
    return new ShoppingCart(cartId, new ArrayList<>(), false);
  }

  @PostMapping("/add") (6)
  public Effect<String> addItem(@RequestBody ShoppingCart.LineItem item) {
    if (currentState().checkedOut())
      return effects().error("Cart is already checked out.");

    if (item.quantity() <= 0) {
      return effects().error("Quantity for item " + item.productId() + " must be greater than zero.");
    }

    var event = new ItemAdded(item);

    return effects()
      .emitEvent(event) (7)
      .thenReply(newState -> "OK");
  }


  @PostMapping("/items/{productId}/remove") (6)
  public Effect<String> removeItem(@PathVariable String productId) {
    if (currentState().checkedOut())
      return effects().error("Cart is already checked out.");

    return effects()
      .emitEvent(new ItemRemoved(productId)) (7)
      .thenReply(newState -> "OK");
  }

  @PostMapping("/checkout") (6)
  public Effect<String> checkout() {
    if (currentState().checkedOut())
      return effects().error("Cart is already checked out.");

    return effects()
      .emitEvent(new CheckedOut()) (7)
      .thenReply(newState -> "OK");
  }

  @GetMapping() (6)
  public Effect<ShoppingCart> getCart() {
    return effects().reply(currentState());
  }

  @EventHandler (8)
  public ShoppingCart itemAdded(ItemAdded itemAdded) {
    return currentState().addItem(itemAdded.item());
  }

  @EventHandler (8)
  public ShoppingCart itemRemoved(ItemRemoved itemRemoved) {
    return currentState().removeItem(itemRemoved.productId());
  }

  @EventHandler (8)
  public ShoppingCart checkedOut(CheckedOut checkedOut) {
    return currentState().checkOut();
  }
}
1 Each Entity needs a unique logical type name. This must be unique per Kalix service.
2 The entity needs to be addressed by a unique identifier. The @Id declares the name of the path variable that Kalix should use as unique identifier.
3 The @RequestMapping defines the base path to access the entity. Note that the {cartId} matches the value of @Id.
4 ShoppingCartEntity must inherit from kalix.javasdk.eventsourcedentity.EventSourcedEntity.
5 The emptyState method returns the initial state of the shopping cart.
6 External API methods are be exposed as a REST endpoint using Spring’s REST annotations.
7 API methods receive input and validate it. When applicable, an event is emitted.
8 Each emitted event must have a corresponding @EventHandler to update the state of the shopping cart.

Package and deploy your service

To build and publish the container image and then deploy the service, follow these steps:

  1. If you haven’t done so yet, sign in to your Kalix account. If this is your first time using Kalix, this will let you register an account, create your first project, and set this project as the default.

    kalix auth login
  2. Use the deploy target to build the container image, publish it to the container registry as configured in the pom.xml file, and the target kalix:deploy to automatically deploy the service to Kalix:

    mvn deploy kalix:deploy
    If you time stamp your image. For example, <dockerTag>${project.version}-${build.timestamp}</dockerTag> you must always run both targets in one pass, i.e. mvn deploy kalix:deploy. You cannot run mvn deploy first and then mvn kalix:deploy because they will have different timestamps and thus different `dockerTag`s. This makes it impossible to reference the image in the repository from the second target.
  3. You can verify the status of the deployed service using:

    kalix service list

Invoke your service

Once the service has started successfully, you can start a proxy locally to access the service:

kalix service proxy <service name>

You can use command line HTTP clients, such as curl or httpie, to invoke the service through the proxy at localhost:8080, using plaintext connections.

Items can be added to a shopping cart using the /cart/{cartId}/add endpoint on the ShoppingCart service:

curl localhost:8080/cart/123/add \
  --header "Content-Type: application/json" \
  -XPOST \
  --data '{
    "productId": "kalix-tshirt",
    "name": "Kalix Tshirt",
    "quantity": 5
  }'

The state of the cart can be retrieved with /cart/{cartId}.

curl localhost:8080/cart/123

And finally, you can check out the cart with /cart/{cartId}/checkout.

curl -XPOST localhost:8080/cart/123/checkout

You can expose the service to the internet. A generated hostname will be returned from the expose command:

kalix service expose <service name>

Next steps