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:
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.
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
Now, let’s add a booking. We pass in the patient_id
, which is 9082 in this case, to retrieve the created booking.
After clicking Invoke, we successfully fetched the patient details and the bookings.
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.