Implementing Value Entities in Java

Value Entities persist state on every change and Akka Serverless needs to serialize that data to send it to the underlying data store. The most straightforward way to persist the state is with Protocol Buffers using protobuf types. Akka Serverless automatically detects if an updated state is protobuf, and serializes it using protobuf. For other serialization options, including JSON, see Serialization options for Java services.

While Protocol Buffers are the recommended format for persisting state, we recommend that you do not persist your service’s protobuf messages. While this may introduce some overhead to convert from one type to the other, it allows the service public interface logic to evolve independently of the data storage format, which should be private.

For value entities, modify the state and then trigger a save of that state. Both the JavaScript and the Java SDK have a context.updateState(newState) method for this purpose. If you change the state but do not call updateState, that state change is lost.

The steps necessary to implement a Value Entity include:

  1. Defining the API and domain objects in .proto files.

  2. Implementing behavior in command handlers.

  3. Creating and initializing the Entity.

The sections on this page walk through these steps using a counter service as an example.

Defining the proto files

Our Value entity example starts with the "Counter" service as included in Kickstart a Maven project.

The following counter_domain.proto file defines our "Counter" Value Entity. The entity stores an integer value as represented in the message CounterState. Real world entities store much more data — often structured data —  they represent an Entity in the domain-driven design sense of the term.

src/main/proto/value-entities/counter_domain.proto
syntax = "proto3";

package com.example.domain; (1)

import "akkaserverless/annotations.proto"; (2)

option java_outer_classname = "CounterDomain"; (3)

option (akkaserverless.file).value_entity = { (4)
    name: "Counter" (5)
    entity_type: "counter" (6)
    state: "CounterState" (7)
};

message CounterState { (8)
  int32 value = 1;
}
1 Any classes generated from this protobuf file will be in the Java package com.example.domain.
2 Import the Akka Serverless protobuf annotations, or options.
3 Let the messages declared in this protobuf file be inner classes to the Java class CounterDomain.
4 The protobuf option (akkaserverless.file).value_entity is specific to code-generation as provided by the Akka Serverless Maven plugin.
5 name denotes the base name for the Value entity, the code-generation will create initial sources Counter, CounterTest and CounterIntegrationTest. Once these files exist, they are not overwritten, so you can freely add logic to them.
6 enity_type is a unique identifier of the "state storage". The entity name may be changed even after data has been created, the entity_type can’t. This value shows in the @ValueEnity annotation of your entity implementation.
7 state points to the protobuf message representing the Value entity’s state which is kept by Akka Serverless
8 The CounterState protobuf message is what Akka Serverless stores for this entity.

The counter_api.proto file defines the commands we can send to the Counter service to manipulate or access the Counter`s state. They make up the service API:

src/main/proto/value-entities/counter_api.proto
// This is the public API offered by your entity.
syntax = "proto3";

import "google/protobuf/empty.proto";
import "akkaserverless/annotations.proto"; (1)
import "google/api/annotations.proto";

package com.example; (2)

option java_outer_classname = "CounterApi"; (3)

message IncreaseValue { (4)
  string counter_id = 1 [(akkaserverless.field).entity_key = true]; (5)
  int32 value = 2;
}

message DecreaseValue {
  string counter_id = 1 [(akkaserverless.field).entity_key = true];
  int32 value = 2;
}

message ResetValue {
  string counter_id = 1 [(akkaserverless.field).entity_key = true];
}

message GetCounter {
  string counter_id = 1 [(akkaserverless.field).entity_key = true];
}

message CurrentCounter { (6)
  int32 value = 1;
}

service CounterService { (7)
  option (akkaserverless.service) = { (8)
    type : SERVICE_TYPE_ENTITY
    component : ".domain.Counter"
  };

  rpc Increase (IncreaseValue) returns (google.protobuf.Empty);
  rpc Decrease (DecreaseValue) returns (google.protobuf.Empty);
  rpc Reset (ResetValue) returns (google.protobuf.Empty);
  rpc GetCurrentCounter (GetCounter) returns (CurrentCounter);
}
1 Import the Akka Serverless protobuf annotations, or options.
2 Any classes generated from this protobuf file will be in the Java package com.example.
3 Let the messages declared in this protobuf file be inner classes to the Java class CounterApi.
4 We use protobuf messages to describe the Commands that our service handles. They may contain other messages to represent structured data.
5 Every Command must contain a string field that contains the entity ID and is marked with the (akkaserverless.field).entity_key option.
6 Messages describe the return value for our API. For methods that don’t have return values, we use google.protobuf.Empty.
7 The service descriptor shows the API of the entity. It lists the methods a client can use to issue Commands to the entity.
8 The protobuf option (akkaserverless.service) is specific to code-generation as provided by the Akka Serverless Maven plugin and points to the protobuf definition Counter we’ve seen above (in the com.example.domain package).

Implementing behavior

A Value Entity implementation is a Java class annotated with @ValueEntity new tab. The class must have a constructor accepting the entity ID in a parameter annotated with @EntityId new tab.

As we rely on the code-generation, the class Counter gets generated for us, it extends the generated class CounterInterface which we’re not supposed to change as it gets regenerated in case we update the protobuf descriptors.

src/main/java/com/example/domain/Counter.java
/**
 * A Counter represented as a value entity.
 */
@ValueEntity(entityType = "counter") (1)
public class Counter extends AbstractCounter {
    @SuppressWarnings("unused")
    private final String entityId;

    public Counter(@EntityId String entityId) { (2)
        this.entityId = entityId;
    }
1 The @ValueEntity annotation sets the entityType as a unique identifier of the "state storage". The class name may be changed even after data has been created, the entityType can’t.
2 The constructor accepts a parameter marked as the @EntityId and we keep the value in a field.

We need to implement all methods our Value Entity offers as command handlers.

In our case, the code-generation will generate an implementation class with an initial empty implementation which we’ll discuss below.

Command handlers show in the implementation class as methods annotated with @CommandHandler new tab. By default, the name of the command that the method handles is the name of the method with the first letter capitalized. So, a method called getCurrentCounter will handle a gRPC service call command named GetCurrentCounter. This can be overridden by setting the name parameter on the @CommandHandler annotation.

Updating state

In the example below, the Increase service call uses the value from the request message IncreaseValue. It uses the CommandContext new tab method to manage the entity state.

/src/main/java/com/example/domain/Counter.java
@Override
public Reply<Empty> increase(CounterApi.IncreaseValue command, CommandContext<CounterDomain.CounterState> ctx) {
    if (command.getValue() < 0) { (1)
        throw ctx.fail("Increase requires a positive value. It was [" + command.getValue() + "].");
    }
    CounterDomain.CounterState state = ctx.getState() (2)
            .orElseGet(() -> CounterDomain.CounterState.newBuilder().build()); (3)
    CounterDomain.CounterState newState =  (4)
            state.toBuilder().setValue(state.getValue() + command.getValue()).build();
    ctx.updateState(newState); (5)
    return Reply.message(Empty.getDefaultInstance());
}
1 The validation ensures acceptance of positive values and it fails calls with illegal values by throwing an exception provided by the ctx.fail(…​) method.
2 We retrieve the existing state via ctx.state() which returns a java.util.Optional<CounterDomain.CounterState>.
3 In case the Optional is empty, we create our initial state.
4 From the initial state we create a new state with the increased value.
5 We store the new state with ctx.updateState.

Retrieving state

The following example shows the implementation of the GetCurrentCounter command handler. This command handler is a read-only command handler—​it doesn’t update the state, it just returns it:

src/main/java/com/example/domain/Counter.java
@Override
public Reply<CounterApi.CurrentCounter> getCurrentCounter(CounterApi.GetCounter command, CommandContext<CounterDomain.CounterState> ctx) {
    CounterApi.CurrentCounter current = ctx.getState() (1)
            .map((state) -> CounterApi.CurrentCounter.newBuilder().setValue(state.getValue()).build()) (2)
            .orElseGet(() -> CounterApi.CurrentCounter.newBuilder().setValue(0).build()); (3)
    return Reply.message(current);
}
1 We access the optional state as above.
2 In case we have state for this entity, we use its value to create the CurrentCounter return value.
3 If there isn’t any state available, we create CurrentCounter for the non-existing entity.

Registering the Entity

To make Akka Serverless aware of the Value Entity, we need to register it with the service.

As our example relies on the code generation, the registration gets automatically inserted via the generated MainComponentRegistrations.withGeneratedComponentsAdded method.

To register a Value Entity it calls the registerValueEntity method. In addition to passing the Value Entity’s class and protobuf service descriptor, it also requires any descriptors that define the state.

package com.example;

import com.akkaserverless.javasdk.AkkaServerless;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.example.MainComponentRegistrations.withGeneratedComponentsAdded;

public final class Main {

    private static final Logger LOG = LoggerFactory.getLogger(Main.class);

    public static final AkkaServerless SERVICE =
            withGeneratedComponentsAdded(new AkkaServerless());

    public static void main(String[] args) throws Exception {
        LOG.info("starting the Akka Serverless service");
        SERVICE.start().toCompletableFuture().get();
    }
}

Testing the Entity

To test this Value Entity first we need to create an instance of a Counter to which we need to pass an entityId of type String.

src/test/java/com/example/domain/CounterTest.java
public class CounterTest {
    private final String entityId = "entityId1";
    private Counter entity;

We also need to emulate the context new tab of this Value Entity in order to verify whether the state has been updated as expected. We do this because we don’t have access to the final state of the entity so listen to how it’s been updated. For this we need to mock it as follows.

src/test/java/com/example/domain/CounterTest.java
private CommandContext<CounterDomain.CounterState> contextWithoutState() {
    CommandContext<CounterDomain.CounterState> context = Mockito.mock(CommandContext.class);
    Mockito.when(context.getState()).thenReturn(Optional.empty());
    return context;
}

What we are mocking here is the method getState() of the public interface CommandContext new tab that, by definition, gets the current state of the Value Entity or empty if none have been created. Therfore, as we can see in the snippet above we are simulating that the entity hasn’t been created yet.

With the Counter, the entityId and the mocked CommandContext we can now create our test.

src/test/java/com/example/domain/CounterTest.java
@Test
public void increaseNoPriorState() {
    entity = new Counter(entityId); (1)
    CommandContext<CounterDomain.CounterState> context = contextWithoutState(); (2)

    CounterApi.IncreaseValue message = CounterApi.IncreaseValue.newBuilder().setValue(42).build(); (3)
    entity.increase(message, context); (4)

    Mockito.verify(context).updateState(CounterDomain.CounterState.newBuilder().setValue(42).build()); (5)
}
1 creating the instance of Counter Value Entity.
2 creating the CommandContext<T> new tab. Where T is the type of the domain message our entity receives.
3 creating the API message to send to the instance.
4 calling to the method under test.
5 verifying the void updateState(T state) method has been called with the expected value. Where T is the type of the domain message our entity receives.

In this test we call to the increase method of that Counter by sending an API message with a mocked CommandContext. We then verify that void updateState(T state) has been called with the expected value.