Using the Cassandra Client

Apache Cassandra® is a free and open-source, distributed, wide column store, NoSQL database management system designed to handle large amounts of data across many commodity servers, providing high availability with no single point of failure.

In this guide, we will see how you can get your REST services to use a Cassandra database.

This extension is developed by a third party and is part of the Quarkus Platform.

Prerequisites

To complete this quickstart guide, you need:

Architecture

This quickstart guide shows how to build a REST application using the Cassandra Quarkus extension, which allows you to connect to an Apache Cassandra, DataStax Enterprise (DSE) or DataStax Astra database, using the DataStax Java driver.

This guide will also use the DataStax Object Mapper – a powerful Java-to-CQL mapping framework that greatly simplifies your application’s data access layer code by sparing you the hassle of writing your CQL queries by hand.

The application built in this quickstart guide is quite simple: the user can add elements in a list using a form, and the items list is updated. All the information between the browser and the server is formatted as JSON, and the elements are stored in the Cassandra database.

Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

The solution is located in the quickstart directory of the Cassandra Quarkus extension GitHub repository.

Creating a Blank Maven Project

First, create a new Maven project and copy the pom.xml file that is present in the quickstart directory.

The pom.xml is importing all the Quarkus extensions and dependencies you need.

Creating the Data Model and Data Access Objects

In this example, we will create an application to manage a list of fruits.

First, let’s create our data model – represented by the Fruit class – as follows:

@Entity
@PropertyStrategy(mutable = false)
public class Fruit {

    @PartitionKey
    private final String name;

    private final String description;

    public Fruit(String name, String description) {
      this.name = name;
      this.description = description;
    }
  // getters, hashCode, equals, toString methods omitted for brevity
}

As stated above, we are using the DataStax Object Mapper. In other words, we are not going to write our CQL queries manually; instead, we will annotate our data model with a few annotations, and the mapper will generate proper CQL queries underneath.

This is why the Fruit class is annotated with @Entity: this annotation marks it as an entity class that is mapped to a Cassandra table. Its instances are meant to be automatically persisted into, and retrieved from, the Cassandra database. Here, the table name will be inferred from the class name: fruit.

Also, the name field represents a Cassandra partition key, and so we are annotating it with @PartitionKey – another annotation from the Object Mapper library.

Entity classes are normally required to have a default no-arg constructor, unless they are annotated with @PropertyStrategy(mutable = false), which is the case here.

The next step is to create a DAO (Data Access Object) interface that will manage instances of Fruit entities:

@Dao
public interface FruitDao {
  @Update
  void update(Fruit fruit);

  @Select
  PagingIterable<Fruit> findAll();
}

This interface exposes operations that will be used in our REST service. Again, the annotation @Dao comes from the DataStax Object Mapper, which will also automatically generate an implementation of this interface for you.

Note also the special return type of the findAll method, PagingIterable: it’s the base type of result sets returned by the driver.

Finally, let’s create the a Mapper interface:

@Mapper
public interface FruitMapper {
  @DaoFactory
  FruitDao fruitDao();
}

The @Mapper annotation is yet another annotation recognized by the DataStax Object Mapper. A mapper is responsible for constructing instances of DAOs – in this case, out mapper is constructing an instance of our only DAO, FruitDao.

Creating a Service & JSON REST Endpoint

Now let’s create a FruitService that will be the business layer of our application and store/load the fruits from the Cassandra database.

@ApplicationScoped
public class FruitService {

  @Inject FruitDao dao;

  public void save(Fruit fruit) {
    dao.update(fruit);
  }

  public List<Fruit> getAll() {
    return dao.findAll().all();
  }
}

Note how the service is being injected a FruitDao instance. This DAO instance is injected automatically.

The Cassandra Quarkus extension allows you to inject any of the following beans in your own components:

  • All @Mapper-annotated interfaces in your project.

  • All @Dao-annotated interfaces in your project, as long as they are produced by a corresponding @DaoFactory-annotated method declared in a mapper interface from your project.

  • The QuarkusCqlSession bean: this application-scoped, singleton bean is your main entry point to the Cassandra client; it is a specialized Cassandra driver session instance with a few methods tailored especially for Quarkus. Read its javadocs carefully!

In our example, both FruitMapper and FruitDao could be injected anywhere. We chose to inject FruitDao in FruitService.

The last missing piece is the REST API that will expose GET and POST methods:

@Path("/fruits")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class FruitResource {

  @Inject FruitService fruitService;

  @GET
  public List<FruitDto> getAll() {
    return fruitService.getAll().stream().map(this::convertToDto).collect(Collectors.toList());
  }

  @POST
  public void add(FruitDto fruit) {
    fruitService.save(convertFromDto(fruit));
  }

  private FruitDto convertToDto(Fruit fruit) {
    return new FruitDto(fruit.getName(), fruit.getDescription());
  }

  private Fruit convertFromDto(FruitDto fruitDto) {
    return new Fruit(fruitDto.getName(), fruitDto.getDescription());
  }
}

Notice how FruitResource is being injected a FruitService instance automatically.

It is generally not recommended using the same entity object between the REST API and the data access layer. These layers should indeed be decoupled and use distinct APIs in order to allow each API to evolve independently of the other. This is the reason why our REST API is using a different object: the FruitDto class – the word DTO stands for "Data Transfer Object". This DTO object will be automatically converted to and from JSON in HTTP messages:

public class FruitDto {

  private String name;
  private String description;

  public FruitDto() {}

  public FruitDto(String name, String description) {
    this.name = name;
    this.description = description;
  }
  // getters and setters omitted for brevity
}

The translation to and from JSON is done automatically by the Quarkus RestEasy extension, which is included in this guide’s pom.xml file. If you want to add it manually to your application, add the below snippet to your application’s ppm.xml file:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
DTO classes used by the JSON serialization layer are required to have a default no-arg constructor.

The conversion from DTO to JSON is handled automatically for us, but we still must convert from Fruit to FruitDto and vice versa. This must be done manually, which is why we have two conversion methods declared in FruitResource: convertToDto and convertFromDto.

In our example, Fruit and FruitDto are very similar, so you might wonder why not use Fruit everywhere. In real life cases though, it’s not uncommon to see DTOs and entities having very different structures.

Connecting to the Cassandra Database

Connecting to Apache Cassandra or DataStax Enterprise (DSE)

The main properties to configure are: contact-points, to access the Cassandra database; local-datacenter, which is required by the driver; and – optionally – the keyspace to bind to.

A sample configuration should look like this:

quarkus.cassandra.contact-points={cassandra_ip}:9042
quarkus.cassandra.local-datacenter={dc_name}
quarkus.cassandra.keyspace={keyspace}

In this example, we are using a single instance running on localhost, and the keyspace containing our data is k1:

quarkus.cassandra.contact-points=127.0.0.1:9042
quarkus.cassandra.local-datacenter=datacenter1
quarkus.cassandra.keyspace=k1

If your cluster requires plain text authentication, you must also provide two more settings: username and password.

quarkus.cassandra.auth.username=john
quarkus.cassandra.auth.password=s3cr3t

Connecting to a DataStax Astra Cloud Database

When connecting to DataStax Astra, instead of providing a contact point and a datacenter, you should provide a so-called secure connect bundle, which should point to a valid path to an Astra secure connect bundle file. You can download your secure connect bundle from the Astra web console.

You will also need to provide a username and password, since authentication is always required on Astra clusters.

A sample configuration for DataStax Astra should look like this:

quarkus.cassandra.cloud.secure-connect-bundle=/path/to/secure-connect-bundle.zip
quarkus.cassandra.auth.username=john
quarkus.cassandra.auth.password=s3cr3t
quarkus.cassandra.keyspace=k1

Advanced Driver Configuration

You can configure other Java driver settings using application.conf or application.json files. They need to be located in the classpath of your application. All settings will be passed automatically to the underlying driver configuration mechanism. Settings defined in application.properties with the quarkus.cassandra prefix will have priority over settings defined in application.conf or application.json.

To see the full list of settings, please refer to the driver settings reference.

Running a Local Cassandra Database

By default, the Cassandra client is configured to access a local Cassandra database on port 9042 (the default Cassandra port).

Make sure that the setting quarkus.cassandra.local-datacenter matches the datacenter of your Cassandra cluster.
If you don’t know the name of your local datacenter, this value can be found by running the following CQL query: SELECT data_center FROM system.local.

If you want to use Docker to run a Cassandra database, you can use the following command to launch one in the background:

docker run --name local-cassandra-instance -p 9042:9042 -d cassandra

Next you need to create the keyspace and table that will be used by your application. If you are using Docker, run the following commands:

docker exec -it local-cassandra-instance cqlsh -e "CREATE KEYSPACE IF NOT EXISTS k1 WITH replication = {'class':'SimpleStrategy', 'replication_factor':1}"
docker exec -it local-cassandra-instance cqlsh -e "CREATE TABLE IF NOT EXISTS k1.fruit(name text PRIMARY KEY, description text)"

You can also use the CQLSH utility to interactively interrogate your database:

docker exec -it local-cassandra-instance cqlsh

Testing the REST API

In the project root directory:

  • Run mvn clean package and then java -jar ./target/cassandra-quarkus-quickstart-*-runner.jar to start the application;

  • Or better yet, run the application in dev mode: mvn clean quarkus:dev.

Now you can use curl commands to interact with the underlying REST API.

To create a fruit:

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"name":"apple","description":"red and tasty"}' \
  http://localhost:8080/fruits

To retrieve fruits:

curl -X GET http://localhost:8080/fruits

Creating a Frontend

Now let’s add a simple web page to interact with our FruitResource.

Quarkus automatically serves static resources located under the META-INF/resources directory. In the src/main/resources/META-INF/resources directory, add a fruits.html file with the contents from this file in it.

You can now interact with your REST service:

  • If you haven’t done yet, start your application with mvn clean quarkus:dev;

  • Point your browser to http://localhost:8080/fruits.html;

  • Add new fruits to the list via the form.

Reactive Programming with the Cassandra Client

The QuarkusCqlSession interface gives you access to a series of reactive methods that integrate seamlessly with Quarkus and its reactive framework, Mutiny.

If you are not familiar with Mutiny, please check Mutiny - an intuitive reactive programming library.

Let’s rewrite our application using reactive programming with Mutiny.

First, let’s to declare another DAO interface that works in a reactive way:

@Dao
public interface ReactiveFruitDao {

  @Update
  Uni<Void> updateAsync(Fruit fruit);

  @Select
  MutinyMappedReactiveResultSet<Fruit> findAll();
}

Note the usage of MutinyMappedReactiveResultSet - it is a specialized Mutiny type converted from the original Publisher returned by the driver, which also exposes a few extra methods, e.g. to obtain the query execution info. If you don’t need anything in that interface, you can also simply declare your method to return Multi: Multi<Fruit> findAll(),

Similarly, the method updateAsync returns a Uni - it is automatically converted from the original result set returned by the driver.

The Cassandra driver uses the Reactive Streams Publisher API for reactive calls. The Quarkus framework however uses Mutiny. Because of that, the CqlQuarkusSession interface transparently converts the Publisher instances returned by the driver into the reactive type Multi. CqlQuarkusSession is also capable of converting a Publisher into a Uni – in this case, the publisher is expected to emit at most one row, then complete. This is suitable for write queries (they return no rows), or for read queries guaranteed to return one row at most (count queries, for example).

Next, we need to adapt the FruitMapper to construct a ReactiveFruitDao instance:

@Mapper
public interface FruitMapper {
  // the existing method omitted

  @DaoFactory
  ReactiveFruitDao reactiveFruitDao();
}

Now, we can create a ReactiveFruitService that leverages our reactive DAO:

@ApplicationScoped
public class ReactiveFruitService {

  @Inject ReactiveFruitDao fruitDao;

  public Uni<Void> add(Fruit fruit) {
    return fruitDao.update(fruit);
  }

  public Multi<Fruit> getAll() {
    return fruitDao.findAll();
  }
}

Finally, we can create a ReactiveFruitResource:

@Path("/reactive-fruits")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ReactiveFruitResource {

  @Inject ReactiveFruitService service;

  @GET
  public Multi<FruitDto> getAll() {
    return service.getAll().map(this::convertToDto);
  }

  @POST
  public Uni<Void> add(FruitDto fruitDto) {
    return service.add(convertFromDto(fruitDto));
  }

  private FruitDto convertToDto(Fruit fruit) {
    return new FruitDto(fruit.getName(), fruit.getDescription());
  }

  private Fruit convertFromDto(FruitDto fruitDto) {
    return new Fruit(fruitDto.getName(), fruitDto.getDescription());
  }
}

The above resource is exposing a new endpoint, reactive-fruits. Its capabilities are identical to the ones that we created before with FruitResource, but everything is handled in a reactive fashion, without any blocking operation.

The getAll() method above returns Multi, and the add() method returns Uni. These types are the same Mutiny types that we met before; they are automatically recognized by the Quarkus reactive REST API, so we don’t need to convert them into JSON ourselves.

To effectively integrate the reactive logic with the REST API, your application needs to declare a dependency to the Quarkus RestEasy Mutiny extension:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-mutiny</artifactId>
</dependency>

This dependency is already included in this guide’s pom.xml, but if you are starting a new project from scratch, make sure to include it.

Testing the Reactive REST API

Run the application in dev mode as explained above, then you can use curl commands to interact with the underlying REST API.

To create a fruit using the reactive REST endpoint:

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"name":"banana","description":"yellow and sweet"}' \
  http://localhost:8080/reactive-fruits

To retrieve fruits with the reactive REST endpoint:

curl -X GET http://localhost:8080/reactive-fruits

Creating a Reactive Frontend

Now let’s add a simple web page to interact with our ReactiveFruitResource. In the src/main/resources/META-INF/resources directory, add a reactive-fruits.html file with the contents from this file in it.

You can now interact with your reactive REST service:

Health Checks

If you are using the Quarkus SmallRye Health extension, then the Cassandra client will automatically add a readiness health check to validate the connection to the Cassandra cluster. This extension is already included in this guide’s pom.xml, but if you need to include it manually in your application, add the following:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

When health checks are available, you can access the /health/ready endpoint of your application and have information about the connection validation status.

Running in dev mode with mvn clean quarkus:dev, if you point your browser to http://localhost:8080/health/ready you should see an output similar to the following one:

{
    "status": "UP",
    "checks": [
        {
            "name": "DataStax Apache Cassandra Driver health check",
            "status": "UP",
            "data": {
                "cqlVersion": "3.4.4",
                "releaseVersion": "3.11.7",
                "clusterName": "Test Cluster",
                "datacenter": "datacenter1",
                "numberOfNodes": 1
            }
        }
    ]
}
If you need health checks globally enabled in your application, but don’t want to activate Cassandra health checks, you can disable Cassandra health checks by setting the quarkus.cassandra.health.enabled property to false in your application.properties.

Metrics

The Cassandra Quarkus client can provide metrics about the Cassandra session and about individual Cassandra nodes. It supports both Micrometer and MicroProfile.

The first step to enable metrics is to add a few additional dependencies depending on the metrics framework you plan to use.

Enabling Metrics with Micrometer

Micrometer is the recommended metrics framework in Quarkus applications.

To enable Micrometer metrics in your application, you need to add the following to your pom.xml.

<dependency>
  <groupId>com.datastax.oss</groupId>
  <artifactId>java-driver-metrics-micrometer</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>

This guide uses Micrometer, so the above dependencies are already included in this guide’s pom.xml.

Enabling Metrics with MicroProfile Metrics

Remove any dependency to Micrometer from your pom.xml, then add the following ones instead:

<dependency>
  <groupId>com.datastax.oss</groupId>
  <artifactId>java-driver-metrics-microprofile</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-smallrye-metrics</artifactId>
</dependency>

Enabling Cassandra Metrics

Even when metrics are enabled in your application, the Cassandra client will not report any metrics, unless you opt-in for this feature. So your next step is to enable Cassandra metrics in your application.properties file.

quarkus.cassandra.metrics.enabled=true

That’s it!

The final (and optional) step is to customize which specific Cassandra metrics you would like the Cassandra client to track. Several metrics can be tracked; if you skip this step, a default set of useful metrics will be automatically tracked.

For the full list of available metric names, please refer to the driver settings reference page; search for the advanced.metrics section. Also, Cassandra driver metrics are covered in detail in the driver manual.

If you do wish to customize which metrics to track, you should use the following properties:

  • quarkus.cassandra.metrics.session.enabled should contain the session-level metrics to enable (metrics that are global to the session).

  • quarkus.cassandra.metrics.node.enabled should contain the node-level metrics to enable (metrics for which each node contacted by the Cassandra client gets its own metric value).

Both properties accept a comma-separated list of valid metric names.

For example, let’s assume that you wish to enable the following three Cassandra metrics:

  • Session-level: session.connected-nodes and session.bytes-sent;

  • Node-level: node.pool.open-connections.

Then you should add the following settings to your application.properties:

quarkus.cassandra.metrics.enabled=true
quarkus.cassandra.metrics.session.enabled=connected-nodes,bytes-sent
quarkus.cassandra.metrics.node.enabled=pool.open-connections

This guide’s application.properties file has already many metrics enabled; you can use its metrics list as a good starting point for exposing useful Cassandra metrics in your application.

When metrics are properly enabled, metric reports for all enabled metrics are available at the /metrics REST endpoint of your application.

Running in dev mode with mvn clean quarkus:dev, if you point your browser to http://localhost:8080/metrics you should see a list of metrics; search for metrics whose names contain cassandra.

For Cassandra metrics to show up, the Cassandra client needs to be initialized and connected; if you are using lazy initialization (see below), you won’t see any Cassandra metrics until your application actually connects and hits the database for the first time.

Running in native mode

If you installed GraalVM, you can build a native image using:

mvn clean package -Dnative

Beware that native compilation can take a significant amount of time! Once the compilation is done, you can run the native executable as follows:

./target/cassandra-quarkus-quickstart-*-runner

You can then point your browser to http://localhost:8080/fruits.html and use your application.

Eager vs Lazy Initialization

This extension allows you to inject either:

  • a QuarkusCqlSession bean;

  • or the asynchronous version of this bean, that is, CompletionStage<QuarkusCqlSession>;

  • or the reactive version of this bean, that is, Uni<QuarkusCqlSession>.

The most straightforward approach is obviously to inject QuarkusCqlSession directly. This should work just fine for most applications; however, the QuarkusCqlSession bean needs to be initialized before it can be used, and this process is blocking.

Fortunately, it is possible to control when the initialization should happen: the quarkus.cassandra.init.eager-init parameter determines if the QuarkusCqlSession bean should be initialized on its first access (lazy) or when the application is starting (eager). The default value of this parameter is false, meaning the init process is lazy: the QuarkusCqlSession bean will be initialized lazily on its first access – for example, when there is a first REST request that needs to interact with the Cassandra database.

Using lazy initialization speeds up your application startup time, and avoids startup failures if the Cassandra database is not available. However, it could also prove dangerous if your code is fully asynchronous, e.g. if you are using reactive routes: indeed, the lazy initialization could accidentally happen on a thread that is not allowed to block, such as a Vert.x event loop thread. Therefore, setting quarkus.cassandra.init.eager-init to false and injecting QuarkusCqlSession should be avoided in these contexts.

If you want to use Vert.x (or any other reactive framework) and keep the lazy initialization behavior, you should instead inject only CompletionStage<QuarkusCqlSession> or Uni<QuarkusCqlSession>. When injecting these beans, the initialization process will be triggered lazily, but it will happen in the background, in a non-blocking way, leveraging the Vert.x event loop. This way you don’t risk blocking the Vert.x thread.

Alternatively, you can set quarkus.cassandra.init.eager-init to true: in this case the session bean will be initialized eagerly during application startup, on the Quarkus main thread. This would eliminate any risk of blocking a Vert.x thread, at the cost of making your startup time (much) longer.

Conclusion

Accessing a Cassandra database from a client application is easy with Quarkus and the Cassandra extension, which provides configuration and native support for the DataStax Java driver for Apache Cassandra.