Implementing Value Entities

Value Entities persist state on every change and thus Kalix needs to serialize that data to send it to the underlying data store. However, we recommend that you do not persist your service’s public API messages. Persisting private API messages may introduce some overhead when converting from a public message to an internal one but it allows the logic of the service public interface to evolve independently of the data storage format, which should be private.

Kalix Value Entities have nothing in common with the domain-driven design concept of Value Objects. The Value in the name refers to directly modifying a value for the entity’s state - in contrast to Event-sourced Entities that persist events and the entity state is derived from them.

The steps necessary to implement a Value Entity include:

  1. Defining the API and model the entity’s state.

  2. Implementing behavior in command handlers.

  3. Creating and initializing the Entity.

The following sections walk through these steps using a counter service as an example.

Modeling the Entity

As mentioned above, to help us illustrate a Value Entity, you will be implementing a Counter service. For such service, you will want to be able to set the initial counter value but also to increase the counter modifying its state. The state will be a simple Integer but you will use a wrapper class Number for our API, as shown below:

src/main/java/com/example/Number.java
package com.example;

public record Number(Integer value) {}

Identifying the Entity

In order to interact with an Entity in Kalix, we need to assign an type id and one or more instance ids:

  • type id is a unique identifier for all entities of a given type. To define the entity type id, the entity class must be annotated with @TypeId and have a unique and stable identifier assigned.

  • id, on the other hand, is unique per instance. In most cases, the entity id is passed as a path parameter of a REST request. The exception to the rule is when we request Kalix to auto-generate a id for us. In such a case, Kalix won’t try to extract the id from the endpoint path.

The entity id can be defined in different ways, as detailed below.

Single identifier

The most common use is to annotate the class with @Id and assign one path variable name to it. For instance, @Id("id") will instruct Kalix to look up a matching path variable. For an endpoint defined with @RequestMapping("/users/{id}"), Kalix will extract whatever path segment is used to replace {id} and treat it as the Entity unique identifier.

Composite identifier

It’s also possible to have a composite identifier. For example, @Id({"groupId", "id"}) defines a composite identifier made of groupId and id. In such a case, the endpoints for this entity will need to have both path variables, e.g.: @RequestMapping("/users/{groupId}/{id}").

Generated identifier

Finally, you can ask Kalix to generate an unique identifier, this is typically useful when creating an Entity, and the id is a surrogate id. To indicate to Kalix that an Entity id should be generated rather than extracted from the path, be sure to annotate the corresponding command method with @GenerateId. Typically, an Entity has only one method annotated with @GenerateId. The one that creates the Entity. All other methods will have @Id annotation in order to extract the surrogate id from the endpoint path.

It will often be necessary to access the generated entity id from inside the entities code. This can be done using the EntityContext.entityIdnew tab method.

Kalix generates a UUID version 4 (random) keys. Only version 4 UUIDs are currently supported for generated Entity identifiers.

Value Entity’s Effect API

The Value Entity’s Effect defines the operations that Kalix should perform when an incoming command is handled by a Value Entity.

A Value Entity Effect can either:

  • update the entity state and send a reply to the caller

  • directly reply to the caller if the command is not requesting any state change

  • rejected the command by returning an error

  • instruct Kalix to delete the entity

Implementing behavior

Now that you have a good idea of what we want to build and what its API looks like, you will need to:

  • Declare your entity and pick an entity id (it needs to be a unique identifier).

  • Define an access point (i.e. a route path) to your entity.

  • Implement how each command is handled.

Let us start by showing how to create the Value Entity:

src/main/java/com/example/CounterEntity.java
@TypeId("counter") (1)
@Id("counter_id") (2)
public class CounterEntity extends ValueEntity<Integer> { (3)

  @Override
  public Integer emptyState() { return 0; } (4)
1 Every Entity must be annotated with @TypeId with a stable identifier. This identifier should be unique among the different existing entities within a Kalix application.
2 The @Id value should be unique per entity and map to some field being received on the route path, in this example it’s the counter_id.
3 The CounterEntity class should extend kalix.javasdk.valueentity.ValueEntity.
4 The initial state of each counter is defined as 0.

Updating state

We will now show how to add the command handlers for supporting the two desired operations (set and plusOne). Command handlers are implemented as methods on the entity class but are also exposed for external interactions and always return an Effect of some type.

src/main/java/com/example/CounterEntity.java
@PutMapping("/counter/{counter_id}/set") (1)
public Effect<Number> set(@RequestBody Number number) {
  int newCounter = number.value();
  return effects()
      .updateState(newCounter) (2)
      .thenReply(new Number(newCounter)); (3)
}

@PostMapping("/counter/{counter_id}/plusone") (4)
public Effect<Number> plusOne() {
  int newCounter = currentState() + 1; (5)
  return effects()
      .updateState(newCounter) (6)
      .thenReply(new Number(newCounter));
}
1 Expose the set command handler on path /counter/{counter_id}/set as a PUT endpoint where counter_id will be its unique identifier.
2 Set the new counter value to the value received as body of the command request.
3 Reply with the new counter value wrapped within a Number object.
4 The method is accessible as a POST endpoint on /counter/{counter_id}/plusone, where counter_id will be its unique identifier.
5 plusOne increases the counter by adding 1 to the current state.
6 Finally, using the Effect API, you instruct Kalix to persist the new state, and build a reply with the wrapper object.
The counter_id parameter matches the @Id value. Also, for this example, we have opted to always repeat the common route /counter/{counter_id} for each command but a simpler option could be to use a @RequestMethod("/counter/{counter_id}") at class level.

Deleting state

The next example shows how to delete a Value Entity state by returning special deleteEntity() effect.

src/main/java/com/example/CounterEntity.java
@DeleteMapping("/counter/{counter_id}")
public Effect<String> delete() {
  return effects()
      .deleteEntity() (1)
      .thenReply("deleted: " + commandContext().entityId());
}
1 We delete the state by returning an Effect with effects().deleteEntity().

When you give the instruction to delete the entity it will still exist with an empty state from some time. The actual removal happens later to give downstream consumers time to process the change. By default, the existence of the entity is completely cleaned up after a week.

It is not allowed to make further changes after the entity has been "marked" as deleted. You can still handle read requests of the entity until it has been completely removed, but be the current state will be empty.

If you want to make changes after deleting the state you should use the updateState effect with an empty state instead of using deleteEntity.

It is best to not reuse the same entity id after deletion, but if that happens after the entity has been completely removed it will be instantiated as a completely new entity without any knowledge of previous state.

Note that deleting View state must be handled explicitly.

Retrieving state

The following example shows how to implement a simple endpoint to retrieve the current state of the entity, in this case the value for a specific counter.

src/main/java/com/example/CounterEntity.java
@GetMapping("/counter/{counter_id}") (1)
public Effect<Number> get() {
  return effects()
      .reply(new Number(currentState())); (2)
}
1 Expose the get query handler on path /counter/{counter_id} as a GET endpoint where counter_id will be its unique identifier.
2 Reply with the current state wrapped within a Number.

Running Side Effects

An Entity may also emit one or more side effects. A side effect is something whose result has no impact on the result of the current command—​if it fails, the current command still succeeds. The result of the side effect is therefore ignored. When used from inside an Entity, side effects are only performed after the successful completion of any state actions requested by the command handler.

See this dedicated section regarding Actions, for more details.

Testing the Entity

There are two ways to test an Entity:

  • Unit tests, which run the Entity class in the same JVM as the test code itself with the help of a test kit.

  • Integration tests, with the service deployed in a docker container running the entire service and the test interacting with it over HTTP requests.

Each way has its benefits, unit tests are faster and provide more immediate feedback about success or failure but can only test a single entity at a time and in isolation. Integration tests, on the other hand, are more realistic and allow many entities to interact with other components inside and outside the service.

Unit tests

The following snippet shows how the ValueEntityTestKit is used to test the CountertEntity implementation. Kalix provides two main APIs for unit tests, the ValueEntityTestKit and the ValueEntityResult. The former gives us the overall state of the entity and the ability to call the command handlers while the latter only holds the effects produced for each individual call to the Entity.

/src/test/java/com/example/CounterTest.java
@Test
public void testSetAndIncrease() {
  var testKit = ValueEntityTestKit.of(CounterEntity::new); (1)

  var resultSet = testKit.call(e -> e.set(new Number(10))); (2)
  assertTrue(resultSet.isReply());
  assertEquals(10, resultSet.getReply().value()); (3)

  var resultPlusOne = testKit.call(CounterEntity::plusOne); (4)
  assertTrue(resultPlusOne.isReply());
  assertEquals(11, resultPlusOne.getReply().value());

  assertEquals(11, testKit.getState()); (5)
}
1 Creates the TestKit passing the constructor of the Entity.
2 Calls the method set from the Entity in the ValueEntityTestKit with value 10.
3 Asserts the reply value is 10.
4 Calls the method plusOne from the Entity in the ValueEntityTestKit and assert reply value of 11.
5 Asserts the state value after both operations is 11.
The ValueEntityTestKit is stateful, and it holds the state of a single entity instance in memory. If you want to test more than one entity in a test, you need to create multiple instance of ValueEntityTestKit.

Integration tests

The skeleton of an Integration Test is generated for you if you use the archetype to start your Kalix app. Let’s see what it could look like to test our Counter Entity:

/src/it/java/com/example/CounterIntegrationTest.java
@SpringBootTest(classes = Main.class)
public class CounterIntegrationTest extends KalixIntegrationTestKitSupport { (1)

  @Autowired
  private WebClient webClient; (2)

  private Duration timeout = Duration.of(10, SECONDS);

  @Test
  public void verifyCounterSetAndIncrease() {

    Number counterGet = (3)
        webClient
            .get()
            .uri("/counter/bar")
            .retrieve()
            .bodyToMono(Number.class)
            .block(timeout);
    Assertions.assertEquals(0, counterGet.value());

    Number counterPlusOne = (4)
        webClient
            .post()
            .uri("/counter/bar/plusone")
            .retrieve()
            .bodyToMono(Number.class)
            .block(timeout);

    Assertions.assertEquals(1, counterPlusOne.value());

    Number counterGetAfter = (5)
        webClient
            .get()
            .uri("/counter/bar")
            .retrieve()
            .bodyToMono(Number.class)
            .block(timeout);
    Assertions.assertEquals(1, counterGetAfter.value());
  }
}
1 Note the test class must extend KalixIntegrationTestKitSupport.
2 A built-in web-client is provided to interact with the components.
3 Request to get the current value of the counter named bar. Initial value of counter is expected to be 0.
4 Request to increase the value of counter bar. Response should have value 1.
5 Explicit GET request to retrieve value of bar that should be 1.
The integration tests in samples are under in a specific project profile it and can be run using mvn verify -Pit.