Kickstart a Maven project

The Akka Serverless code generation tools for Java help you to get started quickly. They include:

  • A Maven archetype that generates the recommended project structure (including a pom.xml), a simple Counter service containing a Value Entity, and unit tests. A README.md explains what was created and how to work with the newly created service.

  • A Maven plugin that runs the gRPC compiler and generates code stubs. You can modify the .proto and source files, and the Akka Serverless Maven plugin will not overwrite your work but will generate code stubs for the elements you changed or added.

The generated project also contains configuration for packaging and deploying the service.

Prerequisites

Before running the code generation tools, make sure you have the following:

  • JDK 8 or higher (JDK 11 is recommended)

  • Apache Maven 3.6 or later

  • Docker 19.03 or higher (to run locally)

To deploy the generated service, you will need:

1. Generate and build the Akka Serverless project

The Maven archetype prompts you to specify the project’s group ID, name and version interactively. Run it using the commands shown for your OS.

In IntelliJ, you can skip the command line. Open the IDE, select File > New > Project, and click to activate Create from archetype. Use the UI to locate the archetype and fill in the blanks.

Follow these steps to generate and build your project:

  1. From a command window, run the archetype in a convenient location:

    Linux or macOS
    mvn archetype:generate \
      -DarchetypeGroupId=com.akkaserverless \
      -DarchetypeArtifactId=akkaserverless-maven-archetype \
      -DarchetypeVersion=LATEST
    Windows 10+
    mvn archetype:generate ^
      -DarchetypeGroupId=com.akkaserverless ^
      -DarchetypeArtifactId=akkaserverless-maven-archetype ^
      -DarchetypeVersion=LATEST
  2. Navigate to the new project directory.

  3. Enter mvn compile to generate and compile the sources.

As you develop your own logic, you can change the .proto file definitions and build again. The build generates classes and tests as you develop the project, but will not overwrite your work.

2. Examine the Maven project

The archetype created the source files outlined in Process overview. Take a look at the pieces it provided for you:

2.1. Descriptors for the service interface and domain model

Akka Serverless uses gRPC Protocol Buffers language to describe the service interface and the entity domain model. The archetype generates a CounterService API implemented as a Value Entity. The entity descriptors include:

  • src/main/proto/value-entities/counter_api.proto the service API to be used by clients

  • src/main/proto/value-entities/counter_domain.proto the domain model of the Value Entity’s state

Default API protobuf file
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";
import "google/api/annotations.proto";

package com.example;

option java_outer_classname = "CounterApi";

message IncreaseValue {
    string counter_id = 1 [(akkaserverless.field).entity_key = true];
    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 {
    int32 value = 1;
}

service CounterService {
    option (akkaserverless.service) = {
        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);
}
Default domain protobuf file
src/main/proto/value-entities/counter_domain.proto
syntax = "proto3";

package com.example.domain;

import "akkaserverless/annotations.proto";

option java_outer_classname = "CounterDomain";

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

message CounterState {
    int32 value = 1;
}

For more information on descriptors, see Writing gRPC descriptors.

2.2. Component implementation

For the default service description in the archetype, the plugin creates an abstract base class (eg. CounterInterface) which always reflects the latest service description.

Do not modify the base class as it is regenerated on each invocation of mvn compile

On the first build, the plugin creates a Value Entity implementation class where you implement the business logic for command handlers (eg. Counter) .

src/main/java/com/example/domain/Counter.java
package com.example.domain;

import com.akkaserverless.javasdk.EntityId;
import com.akkaserverless.javasdk.valueentity.*;
import com.example.CounterApi;
import com.google.protobuf.Empty;

/** A value entity. */
@ValueEntity(entityType = "counter")
public class Counter extends CounterInterface {
    @SuppressWarnings("unused")
    private final String entityId;

    public Counter(@EntityId String entityId) {
        this.entityId = entityId;
    }

    @Override
    protected Empty increase(CounterApi.IncreaseValue command, CommandContext<CounterDomain.CounterState> ctx) {
        throw ctx.fail("The command handler for `Increase` is not implemented, yet");
    }

    @Override
    protected Empty decrease(CounterApi.DecreaseValue command, CommandContext<CounterDomain.CounterState> ctx) {
        throw ctx.fail("The command handler for `Decrease` is not implemented, yet");
    }

    @Override
    protected Empty reset(CounterApi.ResetValue command, CommandContext<CounterDomain.CounterState> ctx) {
        throw ctx.fail("The command handler for `Reset` is not implemented, yet");
    }

    @Override
    protected CounterApi.CurrentCounter getCurrentCounter(CounterApi.GetCounter command, CommandContext<CounterDomain.CounterState> ctx) {
        throw ctx.fail("The command handler for `GetCurrentCounter` is not implemented, yet");
    }
}

The Maven plugin provides the Main class implementation that registers service components with Akka Serverless.

src/main/java/com/example/Main.java
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();
    }
}

This class is the entry point for running Akka Serverless within the container.

For more details see Implementing Value Entities in Java.

2.3. Unit and integration tests

The Akka Serverless Maven plugin creates a unit test stub for each class. Use this stub as a starting point to test the logic in your implementation. The Akka Serverless Java SDK test kit supports both JUnit 4 and JUnit 5.

src/test/java/com/example/domain/CounterTest.java
package com.example.domain;

import com.akkaserverless.javasdk.valueentity.CommandContext;
import com.example.CounterApi;
import com.google.protobuf.Empty;
import org.junit.Test;
import org.mockito.*;

import static org.junit.Assert.assertThrows;

public class CounterTest {
    private String entityId = "entityId1";
    private Counter entity;
    private CommandContext<CounterDomain.CounterState> context = Mockito.mock(CommandContext.class);

    private class MockedContextFailure extends RuntimeException {};

    @Test
    public void increaseTest() {
        entity = new Counter(entityId);

        Mockito.when(context.fail("The command handler for `Increase` is not implemented, yet"))
            .thenReturn(new MockedContextFailure());

        // TODO: set fields in command, and update assertions to match implementation
        assertThrows(MockedContextFailure.class, () -> {
            entity.increaseWithReply(CounterApi.IncreaseValue.newBuilder().build(), context);
        });
    }

    @Test
    public void decreaseTest() {
        entity = new Counter(entityId);

        Mockito.when(context.fail("The command handler for `Decrease` is not implemented, yet"))
            .thenReturn(new MockedContextFailure());

        // TODO: set fields in command, and update assertions to match implementation
        assertThrows(MockedContextFailure.class, () -> {
            entity.decreaseWithReply(CounterApi.DecreaseValue.newBuilder().build(), context);
        });
    }

    @Test
    public void resetTest() {
        entity = new Counter(entityId);

        Mockito.when(context.fail("The command handler for `Reset` is not implemented, yet"))
            .thenReturn(new MockedContextFailure());

        // TODO: set fields in command, and update assertions to match implementation
        assertThrows(MockedContextFailure.class, () -> {
            entity.resetWithReply(CounterApi.ResetValue.newBuilder().build(), context);
        });
    }

    @Test
    public void getCurrentCounterTest() {
        entity = new Counter(entityId);

        Mockito.when(context.fail("The command handler for `GetCurrentCounter` is not implemented, yet"))
            .thenReturn(new MockedContextFailure());

        // TODO: set fields in command, and update assertions to match implementation
        assertThrows(MockedContextFailure.class, () -> {
            entity.getCurrentCounterWithReply(CounterApi.GetCounter.newBuilder().build(), context);
        });
    }
}

Use the verify command to run all unit tests.

mvn verify

For more details, see Testing the entity.

The Maven plugin also provides you with an initial setup for integration tests based on the Akka Serverless Java SDK test kit which leverages TestContainers and JUnit.

src/it/java/com/example/domain/CounterIntegrationTest.java
package com.example.domain;

import com.example.Main;
import com.example.CounterServiceClient;
import com.akkaserverless.javasdk.testkit.junit.AkkaServerlessTestkitResource;
import org.junit.ClassRule;
import org.junit.Test;

import static java.util.concurrent.TimeUnit.*;

// Example of an integration test calling our service via the Akka Serverless proxy
// Run all test classes ending with "IntegrationTest" using `mvn verify -Pfailsafe`
public class CounterIntegrationTest {

    /**
     * The test kit starts both the service container and the Akka Serverless proxy.
     */
    @ClassRule
    public static final AkkaServerlessTestkitResource testkit = new AkkaServerlessTestkitResource(Main.SERVICE);

    /**
     * Use the generated gRPC client to call the service through the Akka Serverless proxy.
     */
    private final CounterServiceClient client;

    public CounterIntegrationTest() {
        client = CounterServiceClient.create(testkit.getGrpcClientSettings(), testkit.getActorSystem());
    }

    @Test
    public void increaseOnNonExistingEntity() throws Exception {
        // TODO: set fields in command, and provide assertions to match replies
        // client.increase(CounterApi.IncreaseValue.newBuilder().build())
        //         .toCompletableFuture().get(2, SECONDS);
    }

    @Test
    public void decreaseOnNonExistingEntity() throws Exception {
        // TODO: set fields in command, and provide assertions to match replies
        // client.decrease(CounterApi.DecreaseValue.newBuilder().build())
        //         .toCompletableFuture().get(2, SECONDS);
    }

    @Test
    public void resetOnNonExistingEntity() throws Exception {
        // TODO: set fields in command, and provide assertions to match replies
        // client.reset(CounterApi.ResetValue.newBuilder().build())
        //         .toCompletableFuture().get(2, SECONDS);
    }

    @Test
    public void getCurrentCounterOnNonExistingEntity() throws Exception {
        // TODO: set fields in command, and provide assertions to match replies
        // client.getCurrentCounter(CounterApi.GetCounter.newBuilder().build())
        //         .toCompletableFuture().get(2, SECONDS);
    }
}

The Maven failsafe plugin runs the integration tests when the it profile is enabled via -Pit.

mvn verify -Pit

3. Package service

The Maven project is configured to package your service into a Docker image which can be deployed to Akka Serverless. The Docker image name can be changed in the pom.xml file’s properties section. Update this file to publish your image to your Docker repository.

The recommended version is JDK 11 and the image is based on the Eclipse Adoptium JDK image (formerly Adopt OpenJDK). Choose a different image in the docker-maven-plugin configuration pom.xml file.

mvn package

4. Run locally

You can run your service locally for manual testing via HTTP or gRPC requests. To run your application locally, you need to initiate the Akka Serverless proxy. The included docker-compose file contains the configuration required to run the proxy for a locally running application.

To start the proxy, run the following command from the project directory:

MacOS, Windows
docker compose up
Linux
On Linux this requires Docker 20.10 or later (https://github.com/moby/moby/pull/40007), or for a USER_FUNCTION_HOST environment variable to be set manually.
docker compose -f docker-compose.yml -f docker-compose.linux.yml up

Use the exec-maven-plugin to start the application locally with the following command:

mvn compile exec:java

5. Deploy to Akka Serverless

To deploy your service to Akka Serverless:

  1. Update the dockerImage property in the pom.xml to point at your Docker registry.

  2. In your command window, set your Akka Serverless project to be the current project:

    akkasls config set project <project-name>
  3. Run mvn deploy, which conveniently packages and publishes your Docker image prior to deployment.