Actions as Controllers

Actions can be used to implement MVC Controllers by acting as the external interface of a service, receiving requests, operating over the requests values and forwarding the call to other components in the same service.

To illustrate how you can use an Action as a Controller, we will build on top of a Value Entity used to implement a Shopping Cart example, adding a new Action to the existing shopping cart service.

If you are hearing about ValueEntity for the first time, be sure to visit Implementing Value Entities before continuing.

Below you can find a summary of the shopping cart value entity we will use in this chapter: it contains only the signatures of the available endpoints for brevity:

src/main/java/com/example/api/ShoppingCartEntity.java
@EntityKey("cartId")
@EntityType("shopping-cart")
@RequestMapping("/cart/{cartId}") (1)
public class ShoppingCartEntity extends ValueEntity<ShoppingCart> {

  @PostMapping("/create") (2)
  public ValueEntity.Effect<ShoppingCartDTO> create(@PathVariable String cartId) {
    //...

  @PostMapping("/items/add") (3)
  public ValueEntity.Effect<ShoppingCartDTO> addItem(@RequestBody LineItemDTO addLineItem) {
    //...

  @GetMapping (4)
  public ValueEntity.Effect<ShoppingCartDTO> getCart() {
    //...
}
1 Common path being used: /cart/ suffixed with a cartId.
2 POST endpoint exposed at (…​)/create used to create a new cart with cartId.
3 POST endpoint exposed at (…​)/items/add allowing to add an item to an cart.
4 GET endpoint for retrieving the state of a cart.

Forwarding Commands

The forward effect allows you to transform or further validate an incoming request before passing it on to another component and have the response message directly passed back to the client making the request. The response from the forwarded operation must have the same response type as the original request.

In this example it accepts requests with the same structure as the create endpoint listed above, by receiving a LineItemDTO, but add some additional verification of the request and only conditionally forward the request to the entity if the verification is successful:

src/main/java/com/example/api/ShoppingCartController.java
import kalix.javasdk.action.Action;
import kalix.spring.KalixClient;

@RequestMapping("/carts")
public class ShoppingCartController extends Action {

  private final KalixClient kalixClient;

  public ShoppingCartController(KalixClient kalixClient) {
    this.kalixClient = kalixClient; (1)
  }

  @PostMapping("/{cartId}/items/add") (2)
  public Action.Effect<ShoppingCartDTO> verifiedAddItem(@PathVariable String cartId,
                                                        @RequestBody LineItemDTO addLineItem) {
    if (addLineItem.name().equalsIgnoreCase("carrot")) { (3)
      return effects().error("Carrots no longer for sale"); (4)
    } else {
      var deferredCall =
          kalixClient.post("/cart/" + cartId + "/items/add", addLineItem, ShoppingCartDTO.class); (5)
      return effects().forward(deferredCall); (6)
    }
  }
1 KalixClient is injected on the constructor. It will be used to build calls to the underlining Entity.
2 Expose the command handler as a POST endpoint at specified path.
3 Check if the added item is carrots.
4 If it is "carrots" immediately return an error, disallowing adding the item.
5 For allowed requests, use kalixClient to get a deferred call to the entity.
6 The deferredCall is then used with effects().forward() to forward the request to the entity.
You might be wondering what the kalixClient is about. For now, think of it as a lightweight HTTP client allowing you to reach out to other Kalix services. All details can be found at Calling other services chapter.

Transform Request and Response to Another Component

The asyncReply and asyncEffect effects allow you to process and transform a request before calling another component and then also transform the response.

As an example, let us look at the problem of creating a new entity with an id generated by another component.

This example implements an initializeCart command for the controller Action which returns the generated id that can subsequently be used to interact with the cart.

src/main/java/com/example/api/ShoppingCartController.java
  @PostMapping("/create")
  public Action.Effect<String> initializeCart() {
    final String cartId = UUID.randomUUID().toString(); (1)
    CompletionStage<ShoppingCartDTO> shoppingCartCreated =
        kalixClient
            .post("/cart/" + cartId + "/create", "", ShoppingCartDTO.class) (2)
            .execute(); (3)

    // transform response
    CompletionStage<Action.Effect<String>> effect =
        shoppingCartCreated.handle((empty, error) -> { (4)
          if (error == null) {
            return effects().reply(cartId); (5)
          } else {
            return effects().error("Failed to create cart, please retry"); (6)
          }
        });

    return effects().asyncEffect(effect); (7)
  }
1 Generate a new UUID.
2 Use the kalixClient to create a call to endpoint create on the shopping cart - note the use of the full path, empty body and the expected reply type ShoppingCartDTO.
3 execute() on the deferred call immediately triggers a call and returns a CompletionStage for the response.
4 Once the call succeeds or fails the CompletionStage is completed or failed, we can transform the result from CompletionStage<Empty>. to CompletionStage<Effect<String>> using handle.
5 On a successful response, create a reply effect passing back the cartId.
6 If the call leads to an error, create an error effect asking the client to retry.
7 effects().asyncEffect() allows us to reply with a CompletionStage<Effect<String>>.

The action generates a UUID to use as entity id for the shopping cart. UUIDs are extremely unlikely to lead to the same id being generated, but to completely guarantee two calls can never be assigned the same shopping cart we make use of the "boundary of consistency" provided by the entity - the entity will only process a single command at a time and can safely make decisions based on its state - for example to only allow creation once by storing something in its state signifying that it has been created.

In this case you mark that the entity has been created using a creation timestamp in the shopping cart state stored on first create call - when the timestamp has the default value of 0. If the cart has already been stored with a timestamp it returns an error effect:

src/main/java/com/example/api/ShoppingCartEntity.java
  @PostMapping("/create") (2)
  public ValueEntity.Effect<ShoppingCartDTO> create(@PathVariable String cartId) {
    //...
    if (currentState().creationTimestamp() > 0L) {
      return effects().error("Cart was already created");
    } else {
      var newState = currentState().withCreationTimestamp(Instant.now().toEpochMilli());
      return effects()
          .updateState(newState)
          .thenReply(ShoppingCartDTO.of(newState));
    }
  }

Composing calls

The async call shown in the previous section, can also be used to chain or compose multiple calls to a single action response.

This example builds on the previous cart creation by adding an initial item in the cart once it has been created, but before it returns the new id to the client:

src/main/java/com/example/api/ShoppingCartController.java
  @PostMapping("/prepopulated")
  public Action.Effect<String> createPrePopulated() {
    final String cartId = UUID.randomUUID().toString();
    CompletionStage<ShoppingCartDTO> shoppingCartCreated =
        kalixClient.post("/cart/" + cartId + "/create", "", ShoppingCartDTO.class).execute();

    CompletionStage<ShoppingCartDTO> cartPopulated =
        shoppingCartCreated.thenCompose(empty -> { (1)
          var initialItem = new LineItemDTO("e", "eggplant", 1);

          return kalixClient
              .post("/cart/" + cartId + "/items/add", initialItem, ShoppingCartDTO.class) (2)
              .execute(); (3)
        });

    CompletionStage<String> reply = cartPopulated.thenApply(ShoppingCartDTO::cartId); (4)

    return effects()
        .asyncReply(reply); (5)
  }
1 CompletionStage#thenCompose allow you to perform an additional async operation, returning a CompletionStage once the current one completes successfully.
2 Create a request to add an initial item to the cart.
3 Execute the addItem call returns a CompletionStage<ShoppingCartDTO> once it succeeds.
4 Transform the successful completion of addItem with ShoppingCartDTO to the response type of this method - String.
5 effects().asyncReply() lets us reply once the CompletionStage<String> completes.

In this sample it is safe to base a subsequent call to the entity on the reply of the previous one, no client will know of the cart id until createPrePopulated replies.

There is no transaction or consistency boundary outside of the entity, so for a sequence of calls from an action to an entity, the state of the entity could be updated by other calls it receives in-between.

For example, imagine an action that for a cart id retrieves the state using getState to verify if too many items are already in the cart, and once that has been verified, it adds the item to the cart.

src/main/java/com/example/api/ShoppingCartController.java
  @PostMapping("/{cartId}/unsafeAddItem")
  public Action.Effect<String> unsafeValidation(@PathVariable String cartId,
                                                @RequestBody LineItemDTO addLineItem) {
    // NOTE: This is an example of an anti-pattern, do not copy this
    CompletionStage<ShoppingCartDTO> cartReply =
        kalixClient.get("/cart/" + cartId, ShoppingCartDTO.class).execute(); (1)

    CompletionStage<Action.Effect<String>> effect = cartReply.thenApply(cart -> {
      int totalCount = cart.items().stream()
          .mapToInt(LineItemDTO::quantity)
          .sum();

      if (totalCount < 10) {
        return effects().error("Max 10 items in a cart");
      } else {
        var addCall = kalixClient.post("/cart/" + cartId + "/items/add", addLineItem, String.class);
        return effects()
            .forward(addCall); (2)
      }
    });

    return effects().asyncEffect(effect);
  }
1 Between this call returning.
2 And this next call to the same entity, the entity could accept other commands that change the total count of items in the cart.

The problem with this is that a POST /cart/my-cart/items/add call directly to the entity happening between the GET /cart/my-cart action returning and the subsequent "addItem" call from the action would lead to more items in the cart than the allowed limit.

Such validation depending on state can only safely be done handling the command inside of the entity.