Example: Shopping Cart Clean Boundaries
This example shows where code belongs. It is intentionally partial; use official Akka SDK samples for complete runnable syntax.
Domain
package com.example.cart.domain;
import java.util.ArrayList;
import java.util.List;
public record ShoppingCart(List<CartItem> items) {
public static ShoppingCart empty() {
return new ShoppingCart(List.of());
}
public ShoppingCart add(CartItem item) {
if (item.quantity() <= 0) {
throw new InvalidCartItem("Quantity must be positive");
}
var updated = new ArrayList<>(items);
updated.add(item);
return new ShoppingCart(List.copyOf(updated));
}
public boolean isEmpty() {
return items.isEmpty();
}
}The domain model has no Akka imports and can be tested directly.
Application
package com.example.cart.application;
import akka.Done;
import akka.javasdk.annotations.Component;
import akka.javasdk.keyvalueentity.KeyValueEntity;
import com.example.cart.domain.CartItem;
import com.example.cart.domain.ShoppingCart;
@Component(id = "shopping-cart")
public class ShoppingCartEntity extends KeyValueEntity<ShoppingCart> {
public record AddItem(String productId, String name, int quantity) {}
@Override
public ShoppingCart emptyState() {
return ShoppingCart.empty();
}
public Effect<Done> addItem(AddItem command) {
var item = new CartItem(command.productId(), command.name(), command.quantity());
return effects()
.updateState(currentState().add(item))
.thenReply(Done::done);
}
public ReadOnlyEffect<ShoppingCart> getCart() {
return effects().reply(currentState());
}
}The entity owns persistence and effects. It delegates the business rule to the domain model.
API
package com.example.cart.api;
import akka.Done;
import akka.javasdk.annotations.Acl;
import akka.javasdk.annotations.http.Get;
import akka.javasdk.annotations.http.HttpEndpoint;
import akka.javasdk.annotations.http.Put;
import akka.javasdk.client.ComponentClient;
import com.example.cart.application.ShoppingCartEntity;
@Acl(allow = @Acl.Matcher(principal = Acl.Principal.INTERNET))
@HttpEndpoint("/carts")
public class ShoppingCartEndpoint {
private final ComponentClient componentClient;
public ShoppingCartEndpoint(ComponentClient componentClient) {
this.componentClient = componentClient;
}
@Put("/{cartId}/items")
public Done addItem(String cartId, AddItemRequest request) {
return componentClient
.forKeyValueEntity(cartId)
.method(ShoppingCartEntity::addItem)
.invoke(request.toCommand());
}
@Get("/{cartId}")
public CartResponse getCart(String cartId) {
var cart = componentClient
.forKeyValueEntity(cartId)
.method(ShoppingCartEntity::getCart)
.invoke();
return CartResponse.from(cart);
}
}The endpoint owns transport shape and delegation, not cart rules.
Tests To Write
ShoppingCartTest: adding a valid item, rejecting invalid quantity.ShoppingCartEntityTest: command updates state and read-only query returns it.ShoppingCartEndpointTest: route accepts request DTO and returns response DTO.
Review Questions
- Can the cart rules be tested without Akka?
- Is the component id stable and business-named?
- Does the endpoint expose internal state or a public response DTO?
- Is invalid input rejected at the right boundary?
- Would a duplicate
addItemcommand be safe for the business case?