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. This may introduce some overhead to convert from one type to an internal one but allows the service public interface logic to evolve independently of the data storage format, which should be private.

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) {}

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 key (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
@EntityType("counter") (1)
@EntityKey("counter_id") (2)
public class CounterEntity extends ValueEntity<Integer> { (3)

  @Override
  public Integer emptyState() { return 0; } (4)
1 Every Entity must be annotated with @EntityType with a stable name. This name should be unique among the different existing entities within a Kalix application.
2 The @EntityKey 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.

Generated Entity Keys

In some cases, you may wish to generate an Entity key, this is typically useful when creating an entity, and the key is a surrogate key. To indicate to Kalix that an Entity key should be generated rather than extracted from the path, be sure to annotate the corresponding command method with @GenerateEntityKey. It will often be necessary to access the generated entity key from inside the entities code. This can be done using the EntityContext.entityId new tab method, as exemplified below:

src/main/java/com/example/CounterEntity.java
  @GenerateEntityKey (1)
  @PostMapping("/counter/{number}")
  public Effect<String> create(@PathVariable Integer number) {
    return effects()
        .updateState(number)
        .thenReply(commandContext().entityId()); (2)
  }
1 Annotate the command handler with @GenerateEntityKey.
2 Access the generated id and reply.
This will generate a version 4 (random) UUID for the Entity. Only version 4 UUIDs are currently supported for generated Entity keys.

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 entityKey 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 deleteState() effect.

After deleting the Value Entity it is possible to create it again, with the same entity ID.
src/main/java/com/example/CounterEntity.java
@DeleteMapping("/counter/{counter_id}/delete")
public Effect<String> delete() {
  return effects()
      .deleteState() (1)
      .thenReply("deleted: " + commandContext().entityId());
}
1 We delete the state by returning an Effect with effects().deleteState().

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.

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. For example, actually publishing to a pub/sub topic.

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() {
  ValueEntityTestKit<Integer, CounterEntity> testKit = ValueEntityTestKit.of(CounterEntity::new); (1)
  ValueEntityResult<Number> resultSet = testKit.call(e -> e.set(new Number(10))); (2)
  assertTrue(resultSet.isReply());
  assertEquals(10, resultSet.getReply().value()); (3)

  ValueEntityResult<Number> 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.

Integration tests

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

/src/it/java/com/example/CounterIntegrationTest.java
@RunWith(SpringRunner.class)
@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 are under in a specific project profile it and can be run using mvn verify -Pit.