Implementing Key Value Entities

Key Value Entities are entities that persist the full state on every change. Only the latest state is stored, so we don’t have access to any of the history of changes, unlike the event sourced storage used by Event Sourced Entities.

Key Value Entities are currently not replicated across regions. The data of all Key Value Entities exists only in the primary region. All requests to Key Value Entities in other regions are forwarded to the primary region. This means that if Key Value Entities are used in a multi-region project the primary region should not be changed, since the data will not exist in the new region. Full replication of Key Value Entities is coming soon.
Entity and Workflow sharding

Stateful components, such as Entities and Workflows, offer strong consistency guarantees. Each stateful component can have many instances, identified by ID. Akka distributes them across every service instance in the cluster. We guarantee that there is only one stateful component instance in the whole service cluster. If a command arrives to a service instance not hosting that stateful component instance, the command is forwarded by the Akka Runtime to the one that hosts that particular component instance. This forwarding is done transparently via Component Client logic. Because each stateful component instance lives on exactly one service instance, messages can be handled sequentially. Hence, there are no concurrency concerns, each Entity or Workflow instance handles one message at a time.

TODO: add an illustration

The state of the stateful component instance is kept in memory as long as it is active. This means it can serve read requests or command validation before updating without additional reads from the durable storage. There might not be room for all stateful component instances to be kept active in memory all the time and therefore least recently used instances can be passivated. When the stateful component is used again it recovers its state from durable storage and becomes an active with its system of record in memory, backed by consistent durable storage. This recovery process is also used in cases of rolling updates, rebalance, and abnormal crashes.

Akka 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.

The steps necessary to implement a Key Value Entity include:

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

  2. Creating and initializing the Entity.

  3. Implementing behavior in command handlers.

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 Key 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 Counter as the domain model, as shown below:

public record Counter(int value) {
  public Counter increment(int delta) {
    return new Counter(value + delta);
  }
}
Above we are taking advantage of the Java record to reduce the amount of boilerplate code, but you can use regular classes so long as they can be serialized to JSON (e.g. using Jackson annotations). See Serialization.

Identifying the Entity

In order to interact with an Entity in Akka, we need to assign a component id and an instance id:

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

  • id, on the other hand, is unique per instance. The entity id is used in the component client when calling the entity from for example an Endpoint.

As an example, an entity representing a customer could have the component id customer and a customer entity for a specific customer could have the UUID instance id 8C59E488-B6A8-4E6D-92F3-760315283B6E.

The component id and entity id cannot contain the reserved character |, because that is used internally by Akka as a separator.

Key Value Entity’s Effect API

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

A Key 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

  • instruct Akka to delete the entity

  • return an error message

See also Declarative Effects for more information.

Implementing behavior

Now that we have our Entity state defined, the remaining steps can be summarized as follows:

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

  • Initialize your entity state

  • Implement how each command is handled.

The class signature for our counter entity will look like this:

@ComponentId("counter") (1)
public class CounterEntity extends KeyValueEntity<Counter> { (2)

  @Override
  public Counter emptyState() { return new Counter(0); } (3)
}
1 Every Entity must be annotated with @ComponentId with a stable unique identifier for this entity type.
2 The CounterEntity class should extend akka.javasdk.keyvalueentity.KeyValueEntity.
3 The initial state of each counter is defined with value 0.
The @ComponentId value counter is common for all instances of this entity but must be stable - cannot be changed after a production deploy - and unique across the different entity types in the service.

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.

public Effect<Counter> set(int number) {
  Counter newCounter = new Counter(number);
  return effects()
      .updateState(newCounter) (1)
      .thenReply(newCounter); (2)
}

public Effect<Counter> plusOne() {
  Counter newCounter = currentState().increment(1); (3)
  return effects()
      .updateState(newCounter) (4)
      .thenReply(newCounter);
}
1 Set the new counter value to the value received from the command request.
2 Reply with the new counter value wrapped within a Counter object.
3 plusOne increases the counter by adding 1 to the current state.
4 Finally, using the Effect API, you instruct Akka to persist the new state, and build a reply with the wrapper object.
The only way for a command handler to modify the Entity’s state is using the updateState effect. Any modifications made directly to the state (or instance variables) from the command handler are not persisted. When the Entity is passivated and reloaded, those modifications will not be present.

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.

public Effect<Counter> get() {
  return effects()
      .reply(currentState()); (1)
}
1 Reply with the current state.
We are returning the internal state directly back to the requester. In the endpoint, it’s usually best to convert this internal domain model into a public model so the internal representation is free to evolve without breaking clients code.

Deleting state

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

public Effect<Done> delete() {
  return effects()
      .deleteEntity() (1)
      .thenReply(done());
}
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 for 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 to the entity until it has been completely removed, but the current state will be empty.

If you don’t want to permanently delete an entity, you can instead use the updateState effect with an empty state. This will work the same as resetting the entity to its initial state.

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.

Side Effects

An entity doesn’t perform any external side effects aside from persisting events and replying to the request. Side effects can be handled from the Workflow, Consumer, or Endpoint components that are calling the entity.

Testing the Entity

There are two ways to test an Entity:

  • Unit test, which only runs the Entity component with a test kit.

  • Integration test, running the entire service with a test kit and the test interacting with it using a component client or 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 KeyValueEntityTestKit is used to test the CountertEntity implementation. Akka provides two main APIs for unit tests, the KeyValueEntityTestKit and the KeyValueEntityResult. 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.

@Test
public void testSetAndIncrease() {
  var testKit = KeyValueEntityTestKit.of(CounterEntity::new); (1)

  var resultSet = testKit.call(e -> e.set(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().value()); (5)
}
1 Creates the TestKit passing the constructor of the Entity.
2 Calls the method set from the Entity in the KeyValueEntityTestKit with value 10.
3 Asserts the reply value is 10.
4 Calls the method plusOne from the Entity in the KeyValueEntityTestKit and assert reply value of 11.
5 Asserts the state value after both operations is 11.
The KeyValueEntityTestKit 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 KeyValueEntityTestKit.

Integration tests

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

public class CounterIntegrationTest extends TestKitSupport { (1)

  @Test
  public void verifyCounterSetAndIncrease() {

    Counter counterGet =
      await(
        componentClient (2)
          .forKeyValueEntity("bar")
          .method(CounterEntity::get) (3)
          .invokeAsync()
      );
    Assertions.assertEquals(0, counterGet.value());

    Counter counterPlusOne =
      await(
        componentClient
          .forKeyValueEntity("bar")
          .method(CounterEntity::plusOne) (4)
          .invokeAsync()
      );
    Assertions.assertEquals(1, counterPlusOne.value());

    Counter counterGetAfter = (5)
      await(
        componentClient
          .forKeyValueEntity("bar")
          .method(CounterEntity::get)
          .invokeAsync()
      );
    Assertions.assertEquals(1, counterGetAfter.value());
  }

}
1 Note the test class must extend TestKitSupport.
2 A built-in component client is provided to interact with the components.
3 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 Explicitly request current value of bar. It should be 1.
The integration tests in samples are under a specific project profile it and can be run using mvn verify -Pit.

Exposing entities directly

You would normally not expose Entities, Workflows and Views directly to the outside world, but have an Endpoint or Consumer in front of it.

However, a built-in HTTP endpoint for the component can be enabled. The primary purpose of this component endpoint is for convenience in local development so that you can try the entity with curl without having to define the final HttpEndpoint.

Enable access

To enable the component’s HTTP endpoint you can disable access control checks in application.conf:

src/main/resources/application.conf
akka.javasdk.dev-mode.acl.enabled = false

Alternatively, start with:

mvn compile exec:java -Dakka.javasdk.dev-mode.acl.enabled=false

Note that this acl configuration is only used when running locally and not in production. To enable component endpoints in production you must add @Acl to the component class:

// Opened up for access from the public internet.
// For actual services meant for production this must be carefully considered,
// and often set more limited
@Acl(allow = @Acl.Matcher(principal = Acl.Principal.INTERNET))

API

The entity is exposed at a fixed path:

/akka/v1.0/entity/<component id>/<entity id>/<method>

In our counter example that is:

curl localhost:9000/akka/v1.0/entity/counter/foo/get