blog

Appointment Booking at Scale is easy with Kalix

The Kalix Team at Lightbend
  • 16 Feb 2023,
  • 10 minute read

Introduction

The COVID-19 pandemic highlighted the need for scalable, easy-to-build testing and vaccine appointment booking systems. The high demand showed manual booking wasn’t feasible or sustainable. As a result, many countries faced severe public health consequences as hospitals struggled to keep up with the cases in part because of vaccine inaccessibility.

There were two main problems:

  • Existing booking systems couldn’t keep up with high demand.
  • Building new, scalable booking system back ends was difficult, slow, and required expertise, to which not all governments and public health agencies have easy access.

So how do we improve our response time in the future?

We can employ modern technological solutions like Kalix. Kalix can help mitigate those issues by offering development teams the ability to quickly build scalable backends using their existing skills. This makes it a perfect fit for launching booking systems that can handle millions of users immediately.

In this tutorial, we’ll build a serverless back end for a booking service system and explain how Kalix can be a game-changer for development teams in the future.

Getting Started

Kalix is a platform-as-a-service to build scalable backends using the language of your choice. It supports Java, Scala, JavaScript, TypeScript, and Python. Additionally, it removes the unnecessary burden of database management, message brokers, service meshes, security, caching, service discovery, and Kubernetes clusters.

Before we start building out a back end, here are the necessary prerequisites:

  • A free Kalix Account
  • The Kalix CLI
  • Docker 20.10.8 or higher
  • A Docker repository to push the Docker image
  • Java 11 or higher
  • Maven 3.x or higher
  • grpcurl

Building the Back End

Let’s start with making a directory for our hands-on tutorial. Type this command into your integrated development environment (IDE):

mkdir BookingSystem

Navigate to that directory or open that directory using your favorite editor.

cd BookingSystem

You can use the Maven archetype and a Maven plugin, which can help you generate the code according to your needs. For a quick start, run this command and enter the appropriate answers to the prompts.

mvn archetype:generate -DarchetypeGroupId=io.kalix -DarchetypeArtifactId=kalix-maven-archetype -DarchetypeVersion=1.0.7

We’ll use protocol buffers to define our API, the domain, and views. Protocol buffers are an open-source data format used to serialize structured data, which Kalix uses. To do that, we need to create three directories first:

mkdir -p ./src/main/proto/booking/service/api
mkdir -p ./src/main/proto/booking/service/domain
mkdir -p ./src/main/proto/booking/service/view

Defining the API

Inside the src/main/proto/booking/service/api subdirectory, create a booking_service_api.proto file. Then, inside the file, add some declarations:

syntax = "proto3"; //protobuf syntax version

package booking.service.api; //package name

option java_outer_classname = "BookingServiceApi"; //outer classname
import "kalix/annotations.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";

Now we must create a service endpoint annotated to kalix.codegen.

Kalix supports various types of the state model. In this case, we’ll use the Event Sourced Entity State model to define the events because it tracks changes to data, which is important for a booking system. Another benefit of using the Event Sources Entity model is that every entity is scalable and long-lived. Also, it stores the changes into the journal, which is useful for auditing.

Event sourcing is an often misunderstood and hard to implement application architectural pattern. Fortunately, Kalix makes it simple.

Add this code to the booking_service_api.proto file:

service BookingService {
option (kalix.codegen) = {
  event_sourced_entity: {
    name: "booking.service.domain.BookingServiceEntity"
    entity_type: "eventsourced-booking-service"
    state: "booking.service.domain.PatientState"
    events: [
      "booking.service.domain.BookingAdded", 
      "booking.service.domain.BookingRemoved",
      "booking.service.domain.PatientAdded"]
  }
};

Inside the event_sourced_entity object, we define the name, entity_type,state, and events. This tutorial creates three events, one to add a booking, one to remove the booking, and one to add a patient.

Next, write the required remote procedure calls (RPC) or APIs. Thegoogle.api.http annotation exposes the service to the HTTP endpoint alongside the already exposed gRPC endpoint.

rpc CreateBooking(AddBookingCommand) returns (google.protobuf.Empty) {
option (google.api.http) = {
  post: "/patient/{patient_id}/bookings/add"
  body: "*"
};
}

rpc DeleteBooking(DeleteBookingCommand) returns (google.protobuf.Empty) {
option (google.api.http).post = "/patient/bookings/{booking_id}/remove";
}

rpc GetBooking(GetBookingRequest) returns (GetBookingDetails) {
option (google.api.http) = {
  get: "/bookings/{booking_id}"
};
}

We then define messages to define the fields to use. We’ll start with creating messages we will pass to the event RPCs.

message AddBookingCommand {
string booking_id = 1 [(kalix.field).entity_key = true];
string patient_id = 2;
string type = 3;
string date = 4;
}

message DeleteBookingCommand {
string booking_id = 1 [(kalix.field).entity_key = true];
}

message GetBookingRequest {
string booking_id = 1 [(kalix.field).entity_key = true];
}

message GetBookingDetails {
string booking_id = 1 [(kalix.field).entity_key = true];
string patient_id = 2;
string type = 3;
string date = 4;
}

For the fields with the value (kalix.field).entity_key = true, Kalix uses the respective field’s value in order to correctly route the messages.

After these steps, the final booking_service_api.proto file should match this file. The next step is to define the domain.

Defining the Domain

Like the previous step, inside the src/main/proto/booking/service/domain subdirectory, create a booking_service_domain.proto file, then add these declarations:

syntax = "proto3";
package booking.service.domain;
option java_outer_classname = "BookingServiceDomain";

After that, we add messages for entity and event data as follows:

message BookingState {
string patient_id = 1;
string booking_id = 2;
string type = 3;
string date = 4;
}

message BookingAdded {
string patient_id = 1;
string booking_id = 2;
string type = 3;
string date = 4;
}

message BookingRemoved {
string booking_id = 1;
}

After following these steps, the final file should look like this.

Defining the Views

We will use views for retrieving booking details. Create a new file called bookings_by_patient.proto in the src/main/proto/booking/service/view directory and add the following code in it:

syntax = "proto3";

package booking.service.view;

import "google/api/annotations.proto";
import "google/protobuf/any.proto";
import "kalix/annotations.proto";
import "booking/service/api/booking_service_api.proto";
import "booking/service/domain/booking_service_domain.proto";

option java_outer_classname = "BookingsByPatientModel";

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


rpc GetBookingsByPatient(BookingsByPatientRequest) returns (BookingsByPatientResponse) {
  option (kalix.method).view.query = {
    query: "SELECT * AS bookings"
        "  FROM bookings_by_patient"
        " WHERE patient_id = :patient_id"
        " ORDER BY date DESC"
  };
  option (google.api.http) = {
    get: "/bookings-by-patient"
  };
}

rpc OnBookingCreated(domain.BookingAdded) returns (domain.BookingState) {
  option (kalix.method).eventing.in = {
    event_sourced_entity: "booking"
  };
  option (kalix.method).view.update = {
    table: "bookings_by_patient"
    transform_updates: true
  };
}

rpc OnBookingDeleted(domain.BookingRemoved) returns (domain.BookingState) {
  option (kalix.method).eventing.in = {
    event_sourced_entity: "booking"
  };
  option (kalix.method).view.update = {
    table: "bookings_by_patient"
    transform_updates: true
  };
}

message BookingsByPatientRequest { 
  string patient_id = 1; 
}

message BookingsByPatientResponse { 
  repeated domain.BookingState bookings = 1; 
}
}

Just like we did for the API file, we have RPCs and messages being passed to them. The RPCs will be used when we want to retrieve a booking and after we have created or deleted one.

Compiling the Project

The next step is to run a Maven command to generate the classes and the get and set methods. It will throw build errors if anything is wrong in the .proto files.

mvn compile

You should get a result like this:

Build Success

Writing Business Logic

The compile command creates various files and methods to support the development process. We’ll be writing the logic inside the src/main/java/booking/service/domain/Bookings.java file, which the compile command has already created.

Initial Booking Service Entity

Now we must write some business logic. We’ll only discuss one method and associated event handlers in this tutorial, but the complete Booking.java file is here, including all the methods, event handlers, and utility functions.

These are the imports to use in the Booking.java file:

package booking.service.domain;

import booking.service.api.BookingServiceApi;

import com.google.protobuf.Empty;
import kalix.javasdk.eventsourcedentity.EventSourcedEntity;
import kalix.javasdk.eventsourcedentity.EventSourcedEntity.Effect;
import kalix.javasdk.eventsourcedentity.EventSourcedEntityContext;

Here’s the complete method of creating the booking for a given patient:

@Override
public Effect<Empty> createBooking(BookingServiceDomain.BookingState currentState, BookingServiceApi.AddBookingCommand command) {

if (currentState.getType().isEmpty()) {
  return effects()
  .emitEvent(BookingServiceDomain.BookingAdded
    .newBuilder()
    .setBookingId(command.getBookingId())
    .setPatientId(command.getPatientId())
    .setDate(command.getDate())
    .setType(command.getType())
    .build())
  .thenReply(newState -> Empty.getDefaultInstance());
} else {
  return effects().error("Booking '%s' already exists".formatted(currentState.getBookingId()));
}
}

This method gets the new booking details from the API and then creates a new booking by firing the bookingAdded event. Once the new booking is created, the bookingAdded event handler applies the changes to the global state.

@Override
public BookingServiceDomain.BookingState bookingAdded(BookingServiceDomain.BookingState currentState, BookingServiceDomain.BookingAdded event) {
return currentState.toBuilder()
  .setBookingId(event.getBookingId())
  .setPatientId(event.getPatientId())
  .setDate(event.getDate())
  .setType(event.getType())
  .build();
}

This event handler uses several utility functions to improve overall code readability. In the final code, you’ll see we similarly have method handlers for removing the booking, creating a patient, and getting the bookings associated with a patient. We also have two other event handlers to delete the booking and create patient events.

The same logic is used for the views file. The code for the view is found here.

Deploy and Test

Remember, one of the requirements of this project was to install the Kalix CLI. It will help to deploy the application easily. Like this:

kalix auth login 

Follow the Kalix documentation to create a new project.

In the root of the project, inside the pom.xml file, replace the dockerImage property with your container image.

Run the command below:

mvn deploy

You can either log in to the developer console or use the CLI to verify the deployed service with this command:

kalix service list

It’s normal for the service to be in “unavailable” status for some time, but reach out to the support team if it persists.

Now is the time to use the grpcurl package to proxy the Kalix service, which opens the gRPC web UI in the localhost port 8080.

kalix service proxy booking-service --grpcui
gRPC web UI

Now, let’s add a booking. We pass in the patient_id, which is 9082 in this case, to retrieve the created booking.

Create Booking

After clicking Invoke, we successfully fetched the patient details and the bookings.

Kalix Login

You can now enter data to the inputs and test them with other services and events.

Next Steps

This tutorial demonstrated how to leverage Kalix to build out scalable back ends in no time. We can use these same techniques in future emergencies to quickly build vaccine booking portals, tools to help people find resources after a natural disaster, match refugees with resources, and more. Now, system backends can scale rapidly to match emergency responses.

You can follow this tutorial to kickstart your serverless application with a more sophisticated architecture with the necessary fields and validations. Try out Kalix for free.

Author Section will go here