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’re new to Kalix, create an account, so you can try out Kalix for free.
-
You’ll need to install the Kalix CLI to deploy from a terminal window.
-
You’ll also need
-
Java 17 or higher
If you want to bypass writing code and jump straight to the deployment:
|
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.
Follow these steps to generate and build your project:
-
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.5.2
- Windows 10+
-
mvn archetype:generate ^ -DarchetypeGroupId=io.kalix ^ -DarchetypeArtifactId=kalix-spring-boot-archetype ^ -DarchetypeVersion=1.5.2
-
Navigate to the new project directory.
-
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.
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
.
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:
-
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
-
Use the
deploy
target to build the container image, publish it to the container registry as configured in thepom.xml
file, and the targetkalix: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 runmvn deploy
first and thenmvn 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. -
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
-
You can learn more about Event Sourced Entities.