blog

Starting with gRPC in Spring with Kalix

The Kalix Team at Lightbend
  • 31 August 2022,
  • 12 minute read

In this article, we’ll walk through integrating a Spring Boot web app with Kalix.

We’ll use Kalix as the entity store for an application with data management done using Google Remote Procedure Call (gRPC) APIs. The gRPC framework enables language-agnostic, open-source, high-performance remote procedure calls (RPCs) between parts of an application. Kalix leverages gRPC as part of its contract-first API service development.

The gRPC framework uses protocol buffers as its interface definition language (IDL). It performs all inter-service communication using protocol buffers in binary format. Compared to text-based data formats like REST and GraphQL, data encoded in binary format is compact and offers several performance benefits.

Additionally, gRPC supports bidirectional, asynchronous data exchange, whereas REST and GraphQL use synchronous data exchange. Therefore, its lightweight serialization and diverse language support make gRPC a suitable choice for enterprise microservice adoption.

To follow along with this tutorial, you’ll need:

Additionally, the full project code can be found here. To get the most out of this article, please download the project and follow along.

Demo Application

Our example is a Spring Boot application for building a pet catalog that will enable us to add a pet with a name and image. These pets can also have an inventory count. The demo saves data in the application store, which it uses to display all available pets on its home page.

Let’s begin by creating a multi-module Maven project. We need to create two child projects under a root project: a Spring Boot web application and a gRPC-based entity application. As such, we’ll use pom packaging. You can check out this article on Maven packaging types if you aren’t familiar with them.

Start by configuring the build at the top level. Maven requires a pom.xml file with a <modules> element listing all subdirectories:

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.example</groupId>
   <artifactId>pet-catalog-parent</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>pom</packaging>
   <description>pet catalog</description>
   <modules>
      <module>pet-catalog-service</module>
      <module>pet-catalog-web-app</module>
   </modules>
</project>

Entity Store

Now we’ll add the pet-catalog-service project to provide persistence capabilities.

Start by adding a folder named pet-catalog-service under the root folder. The project will have Kalix dependencies. As always, be sure to check for the latest Kalix version before starting. Use the following code to generate a project using the Kalix Maven archetype template:

$ mvn -N -B archetype:generate
  -DarchetypeGroupId=io.kalix
  -DarchetypeArtifactId=kalix-maven-archetype
  -DarchetypeVersion=1.0.1
  -DgroupId=com.example
  -DartifactId=pet-catalog-service
  -Dversion=1.0-SNAPSHOT
  -Dpackage=com.example

Removing the Sample Files

Kalix applications expose the API as gRPC services, with the API and entities defined in protocol buffer descriptors. The Kalix sample application defines a simple counter API with two descriptor files called src/main/proto/org/example/domain/counter_domain.proto and src/main/proto/org/example/counter_api.proto.

We don’t use these sample files, so delete them. We’ll replace these files with our own.

Value Entity

The demo saves pet data using basic attributes like name, and image. Start by creating a protobuf message construct for these attributes. The name and image are string types. Optionally, you can provide a domain class wrapper name by using the java_outer_classname attribute; this can simplify how the code reads to others on your team. For more details, refer to the protocol buffers documentation.

syntax = "proto3";

package com.example.domain;

option java_outer_classname = "PetRepositoryApi";

message PetItem {
  string pet_id = 1 [(kalix.field).entity_key = true];
  string name = 2;
  string image = 3;
}

The application uses the message created above to save and retrieve pet information. This is our data schema. To actually save and retrieve data, we need to start by creating an API to offer the two functions. Kalix provides a value entity type—essentially a key-value database—to store data. We need to configure our API to indicate the type of entity we want to manage, as shown below:

service PetCatalogRepository {
  option (kalix.codegen) = {
    value_entity: {
      name: "com.example.domain.PetCatalogEntity"
      entity_type: "pet"
      state: "com.example.domain.Pet"
    }
  };
  rpc Add(PetItem) returns (google.protobuf.Empty);
  rpc GetDetails(PetItemId) returns (PetItem);
}

In the service definition above, the state attribute of the value_entity refers to the data schema used, as defined by our Pet proto definition. The entity_type attribute provides a friendly name for traditional data needs like data lookup, querying, and integrating with the asynchronous event pipelines in Kalix.

The application also needs a way to query all existing pets. Kalix enables views to provide data lookup and projection features.

First, create a view to query all pets:

service AvailablePets {
 option (kalix.codegen) = {
   view: {}
 };

 rpc GetAllPets(google.protobuf.Empty) returns (stream com.example.domain.Pet) {
   option (kalix.method).view.query = {
     query: "SELECT * FROM pet"
   };
 }
}

Now that we've defined all required protos, create a build to generate the required Java classes. The generated code lacks the required implementation but provides the stubs and scaffolding. So, we must override the Entity class to provide the required save and load operations.

public class PetCatalogEntity extends AbstractPetCatalogEntity {

  @Override
  public Effect<Empty> add(PetItemDomain.Pet currentState, PetRepositoryApi.PetItem petItem) {
    PetItemDomain.Pet newPet = currentState.toBuilder()
      .setName(petItem.getName())
      .setImage(petItem.getImage())
      .setCount(1).build();
    return effects()
      .updateState(newPet)
      .thenReply(Empty.getDefaultInstance());
  }
}

The process also generates a Main class, which is responsible for starting the application. We can run the Main class to deploy the application locally. Alternatively, the project template configures the maven-exec-plugin to run the service using the Maven command.

$ mvn exec:exec

Ensure that you provide the Main class name in pom properties. Otherwise, the executable JAR encounters an issue.

<properties>
  <mainClass>com.example.Main</mainClass>
</properties>

Deployment

The pet-catalog-service project build also generates a Docker image for the required service. We need to upload the image to a container registry before deploying it on the Kalix platform. Kalix supports several container registries, but we’ll work with Docker Hub public repositories.

After generating the Docker image, the image needs to be tagged, uploaded, and deployed. Tag it with an appropriate container image name and tag. Then, invoke the push command to upload it to Docker Hub. Maven conveniently has a built-in command that takes care of all these steps at once.

$ mvn deploy

This simple command ensures smooth deployment of your application. Note that it may take several minutes to start the service.

The Kalix dashboard shows the deployed service. You can enter the logs view to find the generated logs.

gRPC in Spring with Kalix

Next, expose the service so that we can invoke it externally. Ensure that you note the generated URL.

$ kalix service expose pet-cat-entity-service
Service 'pet-cat-entity-service' was successfully exposed at: rough-hat-1694.us-east1.kalix.app

Web Application

Now, we’ll add a web application by creating a folder named pet-catalog-web-app under the root folder. The web application project needs Spring and Thymeleaf. We can use Spring Initializer to generate a Maven pom.xml file with the required dependencies. The following snippet shows the required pom.xml file.

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>pet-catalog-parent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <artifactId>pet-catalog-web-app</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>11</source>
          <target>11</target>
          <compilerArgs>
            <arg>-Xlint:deprecation</arg>
          </compilerArgs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Entity Stubs

The web project requires gRPC stubs to connect to the entity service. Start by adding the required domain, service, and view proto files discussed earlier to the pet-catalog-web-app module. This will be done in the proto directory in the module. Then, to access the files in the linked full code, navigate to the pet-catalog-web-app/src/main/proto directory. We need to remove all of the Kalix specific annotations from these files, as shown below.

service AvailablePets {
 rpc GetAllPets(google.protobuf.Empty) returns (stream com.example.domain.Pet) ;
}

Then, we need to compile these proto files in the Maven build cycle. We’ll add protobuf-maven-plugin along with the necessary gRPC dependencies.

<plugin>
  <groupId>org.xolstice.maven.plugins</groupId>
  <artifactId>protobuf-maven-plugin</artifactId>
  <version>0.6.1</version>
  <configuration>
    <protocArtifact>com.google.protobuf:protoc:3.19.2:exe:${os.detected.classifier}</protocArtifact>
    <pluginId>grpc-java</pluginId>
    <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
  </configuration>
  <executions>
    <execution>
      <goals>
        <goal>compile</goal>
        <goal>compile-custom</goal>
      </goals>
    </execution>
  </executions>
</plugin>

After generating the stubs, we can add a gRPC client to connect to the Kalix-based entity service. This is done in a file called GrpcClientService.java in the pet-catalog-web-app/src/main/java/com/example/petcatalog directory.

public class GrpcClientService {

  @Autowired
  ManagedChannel channel;

  public void add(String name, String loc) {
    PetCatalogRepositoryGrpc.PetCatalogRepositoryBlockingStub blockingStub = PetCatalogRepositoryGrpc.newBlockingStub(channel);
    blockingStub.add(PetRepositoryApi.PetItem.newBuilder().setPetId(String.valueOf(System.currentTimeMillis()))
      .setImage(loc).setName(name).build());
  }

  // Removed for Brevity
}

Since the Spring Framework will manage the service, ensure that you annotate it with the @Service element. The gRPC framework connects using a managed channel, so create a Spring-managed bean, providing the host and port of the deployed application.

@Bean
public ManagedChannel createGrpcClient() {
   return ManagedChannelBuilder.forAddress(host, port)
      .build();
}

The host and port values are in theapplication.properties file as shown below:

entity.host=rough-hat-1694.us-east1.kalix.app
entity.port=443

Controller

The Pet-Catalog Controller is responsible for saving and displaying pets’ data. We need a GET request to load data from the entity service and display it using HTML. Additionally, we need a POST request to save the data. Use the code below to create a PetCatalogController element that can support the required GET and POST APIs.

@Controller
public class PetCatalogController {
  @Autowired
  GrpcClientService grpcClientService;

  @GetMapping("/")
  public ModelAndView load() {
    ModelAndView modelAndView = new ModelAndView("pets");
    List<PetItemDomain.Pet> pets = grpcClientService.loadAllPets();
    modelAndView.addObject("pets", pets);
    return modelAndView;
  }

  @PostMapping("/pet")
  public String save(@RequestParam String petName, @RequestParam String petImage) {
    grpcClientService.add(petName, petImage);
    return "redirect:/";
  }
}

The load method provides the Thymeleaf template, pets, and the associated pets’ data to the Spring servlet in the above APIs. The servlet then generates the required HTML blocks and sends back the complete HTML in response.

HTML

Finally, add a Thymeleaf template called pets.html in src/main/resources/template. The template has the following two sections:

  • A form section to add a new pet. It provides fields for the required data, submitted using the POST method.
  • A pets section for displaying all available pet data. Each pet appears in card format by using the image provided.
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
    <title>Pet-Catalog</title>
  </head>
  <body class="container">
    
    <h1>Add a Pet</h1>
    <form class="row" action="/pet" method="post">
      <fieldset>
        <div class="form-group">
          <label for="petNameLabel">Pet Name</label>
          <input type="text" class="form-control" name="petName" id="petNameLabel">
        </div>
        <div class="form-group">
          <label for="petImageLabel">Pet Image Location</label>
          <input type="text" class="form-control" name="petImage" id="petImageLabel">
        </div>
        <button type="submit" class="btn btn-primary">Save</button>
      </fieldset>
    </form>

    <h1>Available Pets</h1>
    <div class="mb-3" th:each="petItem : ${pets}">
      <div class="col-sm-6">
        <div class="card">
          <img th:src="${petItem.image}" class="card-img-top" alt="...">
          <div class="card-body">
            <h5 class="card-title" th:text="${petItem.name}"></h5>
          </div>
        </div>
      </div>
    </div>
    
  </body>
</html>

That’s all there is! Run the application with the spring-boot:run command. The command starts the webserver on port 8080, and you can access the application in a browser using http://localhost:8080/.

gRPC in Spring with Kalix

Summary

This article demonstrates the integration of a Spring Boot web application with a Kalix data store. The article demonstrates basic create, read, update, and delete (CRUD) capabilities by using Value entities.

Kalix also offers advanced data access patterns like Event Sourcing, Command query responsibility segregation (CQRS), and conflict-free replicated data types (CRDTs). In doing so, it removes all of the associated complexities and challenges. There’s no need to set up an upfront database or cache layer performance tuning. These are out-of-the-box features that enable you to build scalable, resilient, data-centric applications.

Try Kalix for free and simplify the way you build scalable applications.

Author Section will go here