Lightweight and blazing-fast Java app using Quarkus and Aiven for Caching

Traditional Java apps are notorious for being large and slow to start. Read on to see how to use Quarkus and Aiven for Caching to make a Kubernetes-friendly Java app.

When building applications that run on Kubernetes, you want the start-up time and footprint to be small. Traditonal Java applications like to consume more than their constrained memory and CPU and you might have seen OOMKilled errors in your production applications. Brace yourself as this blog takes you on a "supersonic" journey with Quarkus (a fast and lightweight Java framework) and Aiven for Caching (fast in-memory data structure store).

Building a small one-time-password application

Imagine that you're building a one-time-password feature as part of a larger project. Your team is using Java, which they're familiar with, and Quarkus as the framework because they want the application to be Kubernetes-compatible (in terms of size and startup time).

Besides a relational database, the team is also using Redis for fast querying of the generated one-time-passwords. This example uses Aiven for Caching and you can sign up for a free trial if you want to try it out.

An important consideration for your team is that both Quarkus and Aiven for Caching are open-source projects.

Before you start

To follow the hands-on portion of this blog, here are the software requirements:

Time to build the app

Create a Maven Project

The following command creates a new Maven project and adds the required extensions to your project. The Quarkus redis-client extension allows the user to connect to a Aiven for Caching server and run Redis commands. The resteasy-jackson extension allows to build RESTful web services and to process JSON data format. The resteasy-mutiny extension helps to build reactive APIs for asynchronous systems.

mvn io.quarkus.platform:quarkus-maven-plugin:3.7.3:create \ -DprojectGroupId=org.acme \ -DprojectArtifactId=one-time-password \ -Dextensions="redis-client,resteasy-jackson" \ -DnoCode cd one-time-password

Getting the Aiven for Caching server details

Create an Aiven for Caching instance. For this exercise, any service plan will do. Once the Aiven for Caching service is running, copy the Service URI from the Overview > Connection information tab.

Go to your favorite code editor and open the one-time-password project. Paste the Service URI to the src > main > resources > application.properties file:

quarkus.redis.hosts=[YOUR REDIS CONNECTION INFORMATION GOES HERE]

If you're running Redis on your local machine, use the following settings:

quarkus.redis.hosts=redis://localhost:6379

Create the Otp POJO (Plain Old Java Object)

The Otp class creates a POJO (Plain Old Java Object) to hold the session_key and otp_value. The generateRandomOtp method generates a random value within a given range.

Create the src/main/java/org/acme/redis/Otp.java file and add the following:

package org.acme.redis; import java.util.Random; public class Otp { final int lowRange = 100000; final int highRange = 999999; public String session_key; public int otp_value; public Otp(String session_key) { this.session_key = session_key; this.otp_value = generateRandomOtp(lowRange, highRange); } public Otp() {} private static int generateRandomOtp(int low, int high) { // Generate random int value from $low to ($high - 1) return low + new Random().nextInt(high - low); } }

Create the Otp Service

You are going to create the OtpService which will play the role of a Redis client. This class will help you perform the GET, EXISTS, SETEX, SETNX, and TTL Redis commands. For documentation on these see the official Redis commands page.

Create the src/main/java/org/acme/redis/OtpService.java file and add the following:

package org.acme.redis; import io.quarkus.redis.datasource.RedisDataSource; import io.quarkus.redis.datasource.keys.KeyCommands; import io.quarkus.redis.datasource.value.ValueCommands; import jakarta.inject.Singleton; @Singleton class OtpService { final long timeInSeconds = 20; RedisDataSource redisDataSource; private final ValueCommands<String, String> commands; private final KeyCommands<String> keyCommands; public OtpService(RedisDataSource redisDataSource) { this.redisDataSource = redisDataSource; commands = redisDataSource.value(String.class); keyCommands = redisDataSource.key(); } public String getOtp(String session_key) { return commands.get(session_key).toString(); } public void newOtp(String session_key) { Otp otp = new Otp(session_key); // SETNX will only create a key if it doesn't already exist // - so we won't overwrite an existing OTP value // Unfortunately SETNX can't set the TTL/expiration time if (commands.setnx(otp.session_key.toString(), String.valueOf(otp.otp_value))) { // Only update TTL/expiration if the OTP value was set commands.setex(otp.session_key.toString(), timeInSeconds, String.valueOf(otp.otp_value)); } } public String getOtpTTL(String session_key) { return Long.toString(keyCommands.ttl(session_key)); } public boolean keyExists(String session_key) { return keyCommands.exists(session_key); } }

Create the Otp Resource

Create the src/main/java/org/acme/redis/OtpResource.java file where you define the HTTP endpoints for your one-time-password service.

Observe the @Inject annotation to easily create the service instance and @Path("/otp") to indicate the creation of the HTTP endpoints. The same endpoint is used for both GET and POST calls. The great thing of using a framework like Quarkus is that without the annotations, you would have to write multiple lines of code to achieve the same outcome.

package org.acme.redis; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.QueryParam; import io.vertx.core.json.JsonObject; @Path("/otp") public class OtpResource { @Inject OtpService service; @GET public JsonObject getOtp(@QueryParam("session_key") String session_key) { // If the key doesn't exist, return an error response rather than the usual // object response if (!service.keyExists(session_key)) { return errorResponse(); } JsonObject result = new JsonObject(); result.put("OTP: ", service.getOtp(session_key)); result.put("TTL: ", service.getOtpTTL(session_key)); return result; } JsonObject errorResponse() { JsonObject result = new JsonObject(); result.put("Message: ", "The OTP key doesn't exist."); return result; } @POST public void newOtp(@QueryParam("session_key") String session_key) { service.newOtp(session_key); } }

Run your new Quarkus app

From the root of your one-time-password project, execute the following command:

./mvnw quarkus:dev

Navigate to http://localhost:8080/otp?session_key=[SOME_KEY], replacing [SOME_KEY] with any text. For your initial run, you'll receive an error response like this:

"Message: ": "The OTP key doesn't exist."

This is because this key doesn't exist in Redis yet. From your terminal, make the following POST request to create your key:

curl --location --request POST 'http://localhost:8080/otp?session_key=[SOME_KEY]'

Now reload your browser and you should receive a random 6-digit number as a one-time-password that expires in 20 seconds. If you wait more than 20 seconds, it will expire and you'll receive the error response again.

If you keep refreshing the page, you'll be getting the same OTP until the (time-to-live) TTL runs out.

You might also realize that you didn't have to do any server setup/handling for this application as Quarkus took care of that for you. Pretty cool, eh?

Further learning

In this blog, I provided you with a getting started experience with Aiven for Caching and demoed a use case by building a simple one-time-password application using the Quarkus framework. The design and implementation of actual one-time-password software is far more complex than the simple application mentioned here; for instance, it would probably not want to give the same OTP to different users.

To learn more about Quarkus:

To learn more about Redis and Aiven for Caching: