Using the project template
Select your preferred language (Java/Scala) above.
The Kalix code generation tools help you to get started quickly. They include:
-
A Maven archetype giter8 template that generates the recommended project structure, 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 An sbt plugin that runs the gRPC compiler and generates code stubs. You can modify the
.proto
and source files, and the Kalix 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 11 or later
-
Apache Maven 3.6 or later sbt 1.3.6 or later
-
Docker 20.10.14 or higher (to run locally)
To deploy the Kalix service, you need:
-
A configured registry in which to publish the service container image. Refer to Configuring registries for more information on how to make your Docker registry available to Kalix.
1. Generate and build the Kalix project
The Maven archetype giter8 template prompts you to specify the project’s group ID, name and version interactively. Run it using the commands shown for your OS.
Follow these steps to generate and build your project:
-
From a command window, run the template in a convenient location:
- Linux or macOS
-
mvn archetype:generate \ -DarchetypeGroupId=io.kalix \ -DarchetypeArtifactId=kalix-maven-archetype \ -DarchetypeVersion=1.5.2
- Windows 10+
-
mvn archetype:generate ^ -DarchetypeGroupId=io.kalix ^ -DarchetypeArtifactId=kalix-maven-archetype ^ -DarchetypeVersion=1.5.2
-
From a command window, run the template in a convenient location:
sbt new lightbend/kalix-value-entity.g8
-
Navigate to the new project directory.
-
Enter
mvn compile
sbt
and runcompile
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 project
The template 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
Kalix 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/com/example/counter_api.proto
the service API to be used by clients -
src/main/proto/com/example/counter_domain.proto
the domain model of the Value Entity’s state
- Default API protobuf file
-
src/main/proto/com/example/counter_api.proto
// This is the public API offered by your entity. syntax = "proto3"; import "google/protobuf/empty.proto"; import "kalix/annotations.proto"; import "google/api/annotations.proto"; package com.example; option java_outer_classname = "CounterApi"; message IncreaseValue { string counter_id = 1 [(kalix.field).id = true]; int32 value = 2; } message DecreaseValue { string counter_id = 1 [(kalix.field).id = true]; int32 value = 2; } message ResetValue { string counter_id = 1 [(kalix.field).id = true]; } message GetCounter { string counter_id = 1 [(kalix.field).id = true]; } message CurrentCounter { int32 value = 1; } service CounterService { option (kalix.codegen) = { value_entity: { name: ".domain.Counter" type_id: "counter" state: ".domain.CounterState" } }; 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/com/example/domain/counter_domain.proto
syntax = "proto3"; package com.example.domain; option java_outer_classname = "CounterDomain"; 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 template, the plugin creates an abstract base class (e.g., AbstractCounter
) which always reflects the latest service description.
Do not modify the base class as it is regenerated on each invocation of mvn compile compile
|
On the first build, the plugin creates a Value Entity implementation class where you implement the business logic for command handlers (e.g., Counter
) .
- Java
-
src/main/java/com/example/domain/Counter.java
/* This code was generated by Kalix tooling. * As long as this file exists it will not be re-generated. * You are free to make changes to this file. */ package com.example.domain; import kalix.javasdk.valueentity.ValueEntityContext; import com.example.CounterApi; import com.google.protobuf.Empty; public class Counter extends AbstractCounter { @SuppressWarnings("unused") private final String entityId; public Counter(ValueEntityContext context) { this.entityId = context.entityId(); } @Override public CounterDomain.CounterState emptyState() { throw new UnsupportedOperationException("Not implemented yet, replace with your empty entity state"); } @Override public Effect<Empty> increase(CounterDomain.CounterState currentState, CounterApi.IncreaseValue command) { return effects().error("The command handler for `Increase` is not implemented, yet"); } @Override public Effect<Empty> decrease(CounterDomain.CounterState currentState, CounterApi.DecreaseValue command) { return effects().error("The command handler for `Decrease` is not implemented, yet"); } @Override public Effect<Empty> reset(CounterDomain.CounterState currentState, CounterApi.ResetValue command) { return effects().error("The command handler for `Reset` is not implemented, yet"); } @Override public Effect<CounterApi.CurrentCounter> getCurrentCounter(CounterDomain.CounterState currentState, CounterApi.GetCounter command) { return effects().error("The command handler for `GetCurrentCounter` is not implemented, yet"); } }
- Scala
-
src/main/scala/com/example/domain/Counter.scala
/* This code was generated by Kalix tooling. * As long as this file exists it will not be re-generated. * You are free to make changes to this file. */ package com.example.domain import kalix.scalasdk.valueentity.ValueEntity import kalix.scalasdk.valueentity.ValueEntityContext import com.example import com.google.protobuf.empty.Empty class Counter(context: ValueEntityContext) extends AbstractCounter { override def emptyState: CounterState = throw new UnsupportedOperationException("Not implemented yet, replace with your empty entity state") override def increase(currentState: CounterState, command: example.IncreaseValue): ValueEntity.Effect[Empty] = effects.error("The command handler for `Increase` is not implemented, yet") override def decrease(currentState: CounterState, command: example.DecreaseValue): ValueEntity.Effect[Empty] = effects.error("The command handler for `Decrease` is not implemented, yet") override def reset(currentState: CounterState, command: example.ResetValue): ValueEntity.Effect[Empty] = effects.error("The command handler for `Reset` is not implemented, yet") override def getCurrentCounter(currentState: CounterState, command: example.GetCounter): ValueEntity.Effect[example.CurrentCounter] = effects.error("The command handler for `GetCurrentCounter` is not implemented, yet") }
The plugin provides the Main
class implementation that registers service components with Kalix.
- Java
-
src/main/java/com/example/Main.java
/* This code was generated by Kalix tooling. * As long as this file exists it will not be re-generated. * You are free to make changes to this file. */ package com.example; import kalix.javasdk.Kalix; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.example.domain.Counter; public final class Main { private static final Logger LOG = LoggerFactory.getLogger(Main.class); public static Kalix createKalix() { // The KalixFactory automatically registers any generated Actions, Views or Entities, // and is kept up-to-date with any changes in your protobuf definitions. // If you prefer, you may remove this and manually register these components in a // `new Kalix()` instance. return KalixFactory.withComponents( Counter::new); } public static void main(String[] args) throws Exception { LOG.info("starting the Kalix service"); createKalix().start(); } }
- Scala
-
src/main/scala/com/example/Main.scala
package com.example import kalix.scalasdk.Kalix import com.example.domain.Counter import org.slf4j.LoggerFactory object Main { private val log = LoggerFactory.getLogger("com.example.Main") def createKalix(): Kalix = { // The KalixFactory automatically registers any generated Actions, Views or Entities, // and is kept up-to-date with any changes in your protobuf definitions. // If you prefer, you may remove this and manually register these components in a // `Kalix()` instance. KalixFactory.withComponents( new Counter(_)) } def main(args: Array[String]): Unit = { log.info("starting the Kalix service") createKalix().start() } }
This class is the entry point for running Kalix within the container.
For more details see Implementing Value Entities.
2.3. Unit and integration tests
The Kalix plugin creates a unit test stub for the Entity. Use this stub as a starting point to test the logic in your implementation. The Kalix Java/Protobuf SDK test kit supports both JUnit 4 and JUnit 5.
- Java
-
src/test/java/com/example/domain/CounterTest.java
/* This code was generated by Kalix tooling. * As long as this file exists it will not be re-generated. * You are free to make changes to this file. */ package com.example.domain; import kalix.javasdk.testkit.ValueEntityResult; import kalix.javasdk.valueentity.ValueEntity; import com.example.CounterApi; import com.google.protobuf.Empty; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import org.junit.jupiter.api.Test; import scala.jdk.javaapi.CollectionConverters; import static org.junit.jupiter.api.Assertions.*; public class CounterTest { @Test public void exampleTest() { CounterTestKit testKit = CounterTestKit.of(Counter::new); // use the testkit to execute a command // of events emitted, or a final updated state: // ValueEntityResult<SomeResponse> result = testKit.someOperation(SomeRequest); // verify the response // SomeResponse actualResponse = result.getReply(); // assertEquals(expectedResponse, actualResponse); // verify the final state after the command // assertEquals(expectedState, testKit.getState()); } @Test public void increaseTest() { CounterTestKit testKit = CounterTestKit.of(Counter::new); // ValueEntityResult<Empty> result = testKit.increase(IncreaseValue.newBuilder()...build()); } @Test public void decreaseTest() { CounterTestKit testKit = CounterTestKit.of(Counter::new); // ValueEntityResult<Empty> result = testKit.decrease(DecreaseValue.newBuilder()...build()); } @Test public void resetTest() { CounterTestKit testKit = CounterTestKit.of(Counter::new); // ValueEntityResult<Empty> result = testKit.reset(ResetValue.newBuilder()...build()); } @Test public void getCurrentCounterTest() { CounterTestKit testKit = CounterTestKit.of(Counter::new); // ValueEntityResult<CurrentCounter> result = testKit.getCurrentCounter(GetCounter.newBuilder()...build()); } }
- Scala
-
src/test/scala/com/example/domain/CounterSpec.scala
/* This code was generated by Kalix tooling. * As long as this file exists it will not be re-generated. * You are free to make changes to this file. */ package com.example.domain import kalix.scalasdk.testkit.ValueEntityResult import kalix.scalasdk.valueentity.ValueEntity import com.example import com.google.protobuf.empty.Empty import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec class CounterSpec extends AnyWordSpec with Matchers { "Counter" must { "have example test that can be removed" in { val testKit = CounterTestKit(new Counter(_)) // use the testkit to execute a command // and verify final updated state: // val result = testKit.someOperation(SomeRequest) // verify the response // val actualResponse = result.getReply() // actualResponse shouldBe expectedResponse // verify the final state after the command // testKit.getState() shouldBe expectedState } "handle command Increase" in { val testKit = CounterTestKit(new Counter(_)) // val result = testKit.increase(example.IncreaseValue(...)) } "handle command Decrease" in { val testKit = CounterTestKit(new Counter(_)) // val result = testKit.decrease(example.DecreaseValue(...)) } "handle command Reset" in { val testKit = CounterTestKit(new Counter(_)) // val result = testKit.reset(example.ResetValue(...)) } "handle command GetCurrentCounter" in { val testKit = CounterTestKit(new Counter(_)) // val result = testKit.getCurrentCounter(example.GetCounter(...)) } } }
Use mvn verify
sbt -DonlyUnitTest test
to run all unit tests.
- Java
-
mvn verify
- Scala
-
sbt -DonlyUnitTest test
By default the integration and unit test are both invoked by
sbt test
. To only run unit tests runsbt -DonlyUnitTest test
, orsbt -DonlyUnitTest=true test
, or set up that value totrue
in the sbt session byset onlyUnitTest := true
and then runtest
For more details, see Testing a Value Entity. Testing an Event Sourced Entity. Testing an Action.
The Maven plugin also provides you with an initial setup for integration tests based on the Kalix Java/Protobuf SDK test kit which leverages TestContainers and JUnit.
package com.example;
import kalix.javasdk.testkit.junit.jupiter.KalixTestKitExtension;
import com.example.domain.CounterDomain;
import com.google.protobuf.Empty;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.Test;
import static java.util.concurrent.TimeUnit.*;
// This class was initially generated based on the .proto definition by Kalix tooling.
//
// As long as this file exists it will not be overwritten: you can maintain it yourself,
// or delete it so it is regenerated as needed.
// Example of an integration test calling our service via the Kalix Runtime
// Run all test classes ending with "IntegrationTest" using `mvn verify -Pit`
public class CounterIntegrationTest {
/**
* The test kit starts both the service container and the Kalix Runtime.
*/
@RegisterExtension
public static final KalixTestKitExtension testKit =
new KalixTestKitExtension(Main.createKalix());
/**
* Use the generated gRPC client to call the service through the Kalix Runtime.
*/
private final CounterService client;
public CounterIntegrationTest() {
client = testKit.getGrpcClient(CounterService.class);
}
@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(5, 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(5, 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(5, 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(5, SECONDS);
}
}
The Maven Failsafe plugin runs the integration tests when the it
profile is enabled via -Pit
.
mvn verify -Pit
By default the integration and unit test are both invoked by sbt test
.
sbt test
3. Package service
The project is configured to package your service into a Docker image which can be deployed to Kalix. The Docker image name can be changed in the pom.xml
file’s properties
section. build.sbt
. Update this file to publish your image to your Docker repository.
This uses 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. build.sbt
.
- Java
-
mvn install
- Scala
-
sbt -Ddocker.username=alice Docker/publish
For more details see Development Process - Package service. |
4. Run locally
You can run your service locally for manual testing via HTTP or gRPC requests.
To start your service locally, run:
- Java
-
mvn kalix:runAll
- Scala
-
sbt runAll
This command will start your Kalix service and a companion Kalix Runtime as configured in docker-compose.yml
file.
If you prefer, you can also start docker-compose directly by running docker-compose up
in one terminal and in another terminal start your Kalix service with:
- Java
-
mvn kalix:run
- Scala
-
sbt run
5. Deploy to Kalix
To deploy your service to Kalix:
First make sure you have updated the dockerImage
property in the pom.xml
to point at your Docker registry. Then:
-
Run
mvn deploy kalix:deploy
.
deploy
packages and publishes your Docker image to your repository and kalix:deploy
deploys the service with your image to Kalix. If kalixProject
is set, that is the project where the service will be deployed. If it is not set, the service will be deployed in your currently selected project.
If you time stamp your image. For example, <dockerTag>${project.version}-${build.timestamp}</dockerTag> you must always run both targets in one pass, i.e. mvn deploy kalix:deploy . You cannot run mvn deploy first and then mvn kalix:deploy because they will have different timestamps and thus different `dockerTag`s. This makes it impossible to reference the image in the repository from the second target.
|
If you time stamp your image. For example, When using the CLI, in your command window, set your Kalix project to be the current project:
kalix config set project <project-name>
-
Run
sbt Docker/publish
which conveniently packages and publishes your Docker image prior to deployment. -
Deploy your service following the instructions at Development Process - Deploy to Kalix.