Complete tutorial of building Spring REST API with Cassandra using Datastax.

Configuration
Java Compilation:
Java Runtime:
Web Server:
Database:
Maven:
Java Compilation:
Java Runtime:
Web Server:
Database:
Gradle:
JDK 17.0.2
JRE OpenJDK 17.0.2
Tomcat 10.1.16
Cassandra 4.1.5
Maven 3.9.6
JDK 17.0.2
JRE OpenJDK 17.0.2
Tomcat 10.1.16
Cassandra 4.1.5
Gradle 8.6
Create the following folder structure:
folder structure of Spring REST API with Cassandra using Datastax
pom.xml: (if using Gradle, replace pom.xml with build.gradle)
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>net.maxjava</groupId> <artifactId>SpringRESTAPICassandraDatastax</artifactId> <version>1.0</version> <packaging>war</packaging> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>6.1.6</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.4</version> </dependency> <dependency> <groupId>com.datastax.oss</groupId> <artifactId>java-driver-core</artifactId> <version>4.17.0</version> </dependency> <dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>3.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> </plugins> </build> </project>
plugins { id 'java' id 'war' } java { sourceCompatibility = 17 targetCompatibility = 17 } group 'net.maxjava' version '1.0' repositories { mavenCentral() } dependencies { implementation group: 'org.springframework', name: 'spring-webmvc', version: '6.1.6' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.4' implementation group: 'com.datastax.oss', name: 'java-driver-core', version: '4.17.0' implementation group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '3.0.0' compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.30' annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.30' }
Some notes about pom.xml:
  • spring-webmvc for necessary Spring REST controllers and its dependencies
  • jackson-databind is for the java object serialization and deserialization
  • java-driver-core is Datastax libraries for communicating with Cassandra
  • jakarta.annotation-api is needed for annotations like @PostConstruct
  • lombok to remove boilerplate code like get and set
  • maven-compiler-plugin is for compiling the code using source version 17 and target version 17
  • A war artifact will be created for deployment to servlet containers
Some notes about build.gradle:
  • spring-webmvc for necessary Spring REST controllers and its dependencies
  • jackson-databind is for the java object serialization and deserialization
  • java-driver-core is Datastax libraries for communicating with Cassandra
  • jakarta.annotation-api is needed for annotations like @PostConstruct
  • lombok to remove boilerplate code like get and set
  • sourceCompatibility and targetCompatibility is for compiling the code using source version 17 and target version 17
  • A war artifact will be created for deployment to servlet containers
web.xml:
<web-app> <display-name>Spring REST API with Cassandra using Datastax</display-name> <servlet> <servlet-name>SpringRESTAPICassandraDatastax</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/myApplicationContext.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>SpringRESTAPICassandraDatastax</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
Some notes about web.xml:
  • org.springframework.web.servlet.DispatcherServlet is the servlet class for Spring
  • /WEB-INF/myApplicationContext.xml is the location of the Spring context configuration file
myApplicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <context:component-scan base-package="controller,repository"/> <mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven> </beans>
Some notes about myApplicationContext.xml:
  • <context:component-scan base-package="controller,repository"/> is for Spring to look for classes in specific packages to create beans
  • <mvc:message-converters> is to specify library for json serialization and deserialization
This tutorial based on the following Cassandra Table structure, assuming the keyspace company exists:
CREATE TABLE company.customer ( last_name text, first_name text, created timestamp, phone text, PRIMARY KEY (last_name, first_name) );
Some notes about the table:
  • last_name column is the partition key, while first_name is the clustering key
  • for this tutorial we just use the partition key (last_name) to retrieve or delete the rows
MyController.java:
package controller; import repository.CassandraRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import model.Customer; import java.util.List; @RestController @RequestMapping("/myapp") public class MyController { @Autowired private CassandraRepository cassandraRepository; @PutMapping("/customer") public void insertCustomer(@RequestBody Customer customer) { cassandraRepository.insertCustomer(customer); } @GetMapping("/customer") public List<Customer> getCustomerByLastName(@RequestParam("lastName") String lastName) { List<Customer> customerList = cassandraRepository.getCustomerByLastName(lastName); return customerList; } @DeleteMapping("/customer") public void deleteCustomerByLastName(@RequestParam("lastName") String lastName) { cassandraRepository.deleteCustomerByLastName(lastName); } }
Some notes about MyController.java:
  • This is Spring REST API which putting @RestController will do the trick (and make sure the package of this class got scanned in bean creation)
  • This class autowire CassandraRepository which is the bean to interact with the database.
  • Providing some popular functions like insertCustomer, getCustomerByLastName and deleteCustomerByLastName to show insert, retrieve and delete
Customer.java:
package model; import lombok.Data; @Data public class Customer { private String lastName; private String firstName; private String created; private String phone; }
Some notes about Customer.java:
  • This is the object used in the request body and response body of the REST APIs
CassandraRepository.java:
package repository; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.cql.ResultSet; import com.datastax.oss.driver.api.core.cql.Row; import com.datastax.oss.driver.api.core.cql.SimpleStatement; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import org.springframework.stereotype.Repository; import model.Customer; import java.net.InetSocketAddress; import java.time.*; import java.util.*; @Repository public class CassandraRepository { private CqlSession cqlSession; private final String tableName = "company.customer"; private final String serverIP = "localhost"; private final int port = 9042; @PostConstruct private void init() { cqlSession = CqlSession.builder() .addContactPoint(new InetSocketAddress(serverIP, port)) .withLocalDatacenter("datacenter1") .build(); } @PreDestroy private void finish() { cqlSession.close(); } public void insertCustomer(Customer customer) { String cql = "INSERT INTO " + tableName + " (last_name,first_name,created,phone) VALUES (:last_name,:first_name,:created,:phone);"; SimpleStatement simpleStatement = SimpleStatement.builder(cql) .addNamedValue("last_name", customer.getLastName()) .addNamedValue("first_name", customer.getFirstName()) .addNamedValue("created", Instant.now()) .addNamedValue("phone", customer.getPhone()) .build(); cqlSession.execute(simpleStatement); } public List<Customer> getCustomerByLastName(String lastName) { List<Customer> customerList = new ArrayList<>(); String cql = "SELECT * FROM " + tableName + " WHERE last_name = :last_name;"; SimpleStatement simpleStatement = SimpleStatement.builder(cql) .addNamedValue("last_name", lastName) .build(); ResultSet resultSet = cqlSession.execute(simpleStatement); for (Row row : resultSet) { Date created = new Date(row.getInstant("created").toEpochMilli()); Customer customer = new Customer(); customer.setLastName(row.getString("last_name")); customer.setFirstName(row.getString("first_name")); customer.setCreated(created.toString()); customer.setPhone(row.getString("phone")); customerList.add(customer); } return customerList; } public void deleteCustomerByLastName(String lastName) { String cql = "DELETE FROM " + tableName + " WHERE last_name = :last_name;"; SimpleStatement simpleStatement = SimpleStatement.builder(cql) .addNamedValue("last_name", lastName) .build(); cqlSession.execute(simpleStatement); } }
Some notes about CassandraRepository.java:
  • @Repository to specify the class as Spring bean
  • @PostConstruct to specify init to run when the class is initiated, which is to create the cqlSession object
  • This class supports insertCustomer, getCustomerByLastName and deleteCustomerByLastName
  • Assume Cassandra is at localhost 9042, with keyspace "company" created, otherwise please use corresponding values
Sample requests and responses(assume the war file is deployed to <CONTEXT_PATH>):
  • Add a new record:
    curl --request PUT '<CONTEXT_PATH>/myapp/customer' --header 'Content-Type: application/json' --data-raw ' { "firstName": "Ethan", "lastName": "Smith", "phone": "310-123-1234" }'
  • Retrieve the record by last name
    curl --request GET '<CONTEXT_PATH>/myapp/customer?lastName=Smith'
    Response:
    [ { "lastName": "Smith", "firstName": "Ethan", "created": "Tue Jun 04 23:40:42 PDT 2022", "phone": "310-123-1234" } ]
  • Add another record with same last name
    curl --request PUT '<CONTEXT_PATH>/myapp/customer' --header 'Content-Type: application/json' --data-raw ' { "firstName": "Tom", "lastName": "Smith", "phone": "650-123-1234" }'
  • Retrieve the record by same last name again
    curl --request GET '<CONTEXT_PATH>/myapp/customer?lastName=Smith'
    Response:
    [ { "lastName": "Smith", "firstName": "Ethan", "created": "Tue Jun 04 23:40:42 PDT 2022", "phone": "310-123-1234" }, { "lastName": "Smith", "firstName": "Tom", "created": "Tue Jun 04 23:48:10 PDT 2022", "phone": "650-123-1234" } ]
  • Deleting records by the last name
    curl --request DELETE '<CONTEXT_PATH>/myapp/customer?lastName=Smith'
  • Retrieve the record by same last name again
    curl --request GET '<CONTEXT_PATH>/myapp/customer?lastName=Smith'
    Response:
    []