Customer Registry in Java/Protobuf
Learn how to create a customer registry in Java, package it into a container, and run it on Kalix.
Before you begin
-
If you’re new to Kalix, create an account so you can try out Kalix for free.
-
You’ll need to install the Kalix CLI to deploy from a terminal window.
-
You’ll also need
-
Java 11 or higher
If you want to bypass writing code and jump straight to the deployment:
|
Writing the Customer Registry
-
From the command line, create a directory for your project.
mkdir customerregistry
-
Change into the project directory.
cd customerregistry
-
Download the
pom.xml
filecurl -OL https://raw.githubusercontent.com/lightbend/kalix-jvm-sdk/main/samples/java-protobuf-customer-registry-quickstart/pom.xml
-
Update the
dockerImage
property (line 13 of thepom.xml
file) with your container registry name.
Define the external API
The Customer Registry service will create or retrieve a customer, including their email, phone number and mailing address. The customer_api.proto
will contain the external API your clients will invoke.
-
In your project, create two directories for your protobuf files,
src/main/proto/customer/domain
andsrc/main/proto/customer/api
.- Linux or macOS
-
mkdir -p ./src/main/proto/customer/api mkdir -p ./src/main/proto/customer/domain
- Windows 10+
-
mkdir src/main/proto/customer/api mkdir src/main/proto/customer/domain
-
Create a
customer_api.proto
file and save it in thesrc/main/proto/customer/api
directory. -
Add declarations for:
-
The protobuf syntax version,
proto3
. -
The package name,
customer.api
. -
The required Java outer classname,
CustomerAPI
. Messages defined in this file will be generated as inner classes. -
Import
google/protobuf/empty.proto
and Kalixkalix/annotations.proto
.src/main/proto/customer/api/customer_api.protosyntax = "proto3"; package customer.api; option java_outer_classname = "CustomerApi"; import "google/protobuf/empty.proto"; import "kalix/annotations.proto";
-
-
Add the service endpoint. The service endpoint is annotated with
kalix.codegen
indicating we want to generate a Value Entity for this service.src/main/proto/customer/api/customer_api.protoservice CustomerService { option (kalix.codegen) = { value_entity: { name: "customer.domain.Customer" type_id: "customers" state: "customer.domain.CustomerState" } }; rpc Create(Customer) returns (google.protobuf.Empty) {} rpc GetCustomer(GetCustomerRequest) returns (Customer) {} }
-
Add messages to define the fields that comprise a
Customer
object (and its compoundAddress
)src/main/proto/customer/api/customer_api.protomessage Customer { string customer_id = 1 [(kalix.field).id = true]; string email = 2; string name = 3; Address address = 4; } message Address { string street = 1; string city = 2; }
-
Add the message that will identify which customer to retrieve for the
GetCustomer
message:src/main/proto/customer/api/customer_api.protomessage GetCustomerRequest { string customer_id = 1 [(kalix.field).id = true]; }
Define the domain model
The customer_domain.proto
contains all the internal data objects (Entities). The Value Entity in this sample is a Key/Value store that stores only the latest updates.
-
Create a
customer_domain.proto
file and save it in thesrc/main/proto/customer/domain
directory. -
Add declarations for the proto syntax and domain package.
-
The package name,
customer.domain
. -
The Java outer classname,
CustomerDomain
.src/main/proto/customer/domain/customer_domain.protosyntax = "proto3"; package customer.domain; option java_outer_classname = "CustomerDomain";
-
-
Add the
CustomerState
message with fields for entity data, and theAddress
message that defines the compound address:src/main/proto/customer/domain/customer_domain.protomessage CustomerState { string customer_id = 1; string email = 2; string name = 3; Address address = 4; } message Address { string street = 1; string city = 2; }
-
Run
mvn compile
from the project root directory to generate source classes in which you add business logic.mvn compile
Create command handlers
Command handlers, as the name suggests, handle incoming requests before persisting them.
-
If it’s not open already, open
src/main/java/customer/domain/Customer.java
for editing. -
Modify the
create
method by adding the logic to handle the command. The complete method should include the following:src/main/java/customer/domain/Customer.java@Override public Effect<Empty> create( CustomerDomain.CustomerState currentState, CustomerApi.Customer command) { CustomerDomain.CustomerState state = convertToDomain(command); return effects().updateState(state).thenReply(Empty.getDefaultInstance()); } private CustomerDomain.CustomerState convertToDomain(CustomerApi.Customer customer) { CustomerDomain.Address address = CustomerDomain.Address.getDefaultInstance(); if (customer.hasAddress()) { address = convertAddressToDomain(customer.getAddress()); } return CustomerDomain.CustomerState.newBuilder() .setCustomerId(customer.getCustomerId()) .setEmail(customer.getEmail()) .setName(customer.getName()) .setAddress(address) .build(); } private CustomerDomain.Address convertAddressToDomain(CustomerApi.Address address) { return CustomerDomain.Address.newBuilder() .setStreet(address.getStreet()) .setCity(address.getCity()) .build(); }
-
The incoming message contains the request data from your client and the command handler updates the state of the customer.
-
The
convertToDomain
methods convert the incoming request to your domain model.
-
-
Modify the
getCustomer
method as follows to handle theGetCustomerRequest
command:src/main/java/customer/domain/Customer.java@Override public Effect<CustomerApi.Customer> getCustomer( CustomerDomain.CustomerState currentState, CustomerApi.GetCustomerRequest command) { if (currentState.getCustomerId().equals("")) { return effects().error("Customer " + command.getCustomerId() + " has not been created."); } else { return effects().reply(convertToApi(currentState)); } } private CustomerApi.Customer convertToApi(CustomerDomain.CustomerState state) { CustomerApi.Address address = CustomerApi.Address.getDefaultInstance(); if (state.hasAddress()) { address = CustomerApi.Address.newBuilder() .setStreet(state.getAddress().getStreet()) .setCity(state.getAddress().getCity()) .build(); } return CustomerApi.Customer.newBuilder() .setCustomerId(state.getCustomerId()) .setEmail(state.getEmail()) .setName(state.getName()) .setAddress(address) .build(); }
-
If that customer doesn’t exist, processing the command fails.
-
If the customer exists, the reply message contains the customer’s information.
-
The
convertToApi
method converts the state of the customer to a response message for your external API.The
src/main/java/customer/Main.java
file already contains the required code to start your service and register it with Kalix.
-
Define the initial entity state
To give the domain model a starting point, the initial state for the entity needs to be defined.
-
Implement the
emptyState
method by returning a default instance of theCustomerState
class:src/main/java/customer/domain/Customer.java@Override public CustomerDomain.CustomerState emptyState() { return CustomerDomain.CustomerState.getDefaultInstance(); }
Package and deploy your service
To build and publish the container image and then deploy the service, follow these steps:
-
If you haven’t done so yet, sign in to your Kalix account. If this is your first time using Kalix, this will let you register an account, create your first project, and set this project as the default.
kalix auth login
-
Use the
deploy
target to build the container image, publish it to the container registry as configured in thepom.xml
file, and use the targetkalix:deploy
to automatically deploy the service to Kalix:mvn deploy kalix:deploy
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 runmvn deploy
first and thenmvn 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. -
You can verify the status of the deployed service using:
kalix service list
Invoke your service
Once the service has started successfully, you can start a proxy locally to access the service:
kalix service proxy <service name> --grpcui
The --grpcui
option also starts and opens a gRPC web UI for exploring and invoking the service (available at http://127.0.0.1:8080/ui/).
Or you can use command line gRPC or HTTP clients, such as grpcurl
or curl
, to invoke the service through the proxy at localhost:8080
, using plaintext connections.
A customer can be created using the Create
method on CustomerService
, in the gRPC web UI, or with grpcurl
:
grpcurl \
-d '{
"customer_id": "abc123",
"email": "someone@example.com",
"name": "Someone",
"address": {
"street": "123 Some Street",
"city": "Somewhere"
}
}' \
--plaintext localhost:8080 \
customer.api.CustomerService/Create
The GetCustomer
method can be used to retrieve this customer, in the gRPC web UI, or with grpcurl
:
grpcurl \
-d '{"customer_id": "abc123"}' \
--plaintext localhost:8080 \
customer.api.CustomerService/GetCustomer
You can expose the service to the internet. A generated hostname will be returned from the expose command:
kalix service expose <service name>
Try to call the exposed service with grpcurl
:
grpcurl \
-d '{"customer_id": "abc123"}' \
<generated hostname>:443 \
customer.api.CustomerService/GetCustomer
Next steps
-
You can learn more about Value Entities.
-
Continue this example by adding Views, which makes it possible to query the customer registry.