Complete tutorial of building Spring Boot REST API using Redis with transaction.

Configuration
Java Compilation:
Java Runtime:
Maven:
Java Compilation:
Java Runtime:
Gradle:
JDK 17.0.2
JRE OpenJDK 17.0.2
Maven 3.9.6
JDK 17.0.2
JRE OpenJDK 17.0.2
Gradle 8.6
Create the following folder structure:
folder structure of Spring Boot REST API using Redis with transaction
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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <groupId>net.maxjava</groupId> <artifactId>SpringBootRESTAPIRedisTransaction</artifactId> <version>1.0</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <scope>compile</scope> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>5.1.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
plugins { id 'java' id 'org.springframework.boot' version '3.2.5' id 'io.spring.dependency-management' version '1.1.4' } group 'net.maxjava' version '1.0' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation group: 'redis.clients', name: 'jedis', version: '5.1.2' compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30' }
Some notes about pom.xml:
  • Use spring-boot-starter-parent as parent POM for Spring Boot application
  • Spring Boot 3.2.5 requires Java 17
  • Use jedis for interacting with Redis
  • lombok is used to remove boilerplate code like get and set
  • Use spring-boot-maven-plugin to re-package all dependencies into a single, executable jar
  • By running mvn clean package, a jar artifact which is ready to run by using java -jar will be created
Some notes about gradle.build:
  • Use org.springframework.boot plugin for Spring Boot application, to make the jar executable and re-package all dependencies into a single jar
  • Spring Boot 3.2.5 requires Java 17
  • Use jedis for interacting with Redis
  • Use io.spring.dependency-management plugin for dependency management
  • lombok is used to remove boilerplate code like get and set
  • By running gradle clean build, a jar artifact which is ready to run by using java -jar will be created
MySpringBootWebApplication.java:
package maxjava; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MySpringBootWebApplication { public static void main(String[] args) { SpringApplication.run(maxjava.MySpringBootWebApplication.class, args); } }
Some notes about MySpringBootWebApplication.java:
  • Mark a class with @SpringBootApplication and pass the class in SpringApplication.run will be good enough to run a Spring Boot application with web server (default setting)
  • We could just put SpringApplication.run inside the static main function and annotate the same class for simplicity
MyController.java:
package maxjava.controller; import maxjava.caching.RedisService; import maxjava.model.Customer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/myapp") public class MyController { @Autowired RedisService redisService; @GetMapping("/hobbyCount") public String getCustomer(@RequestParam("hobby") String hobby) { String result = "Not found"; try { String key = "hobby:count:" + hobby; String value = redisService.getCache(key); if (value != null) { result = value; } } catch (Exception e) { // Handle Exception } return result; } @PostMapping("/customerWithHobby") public void postCustomer(@RequestBody Customer customer) { try { redisService.setCustomerAndCountHobby(customer); } catch (Exception e) { // Handle Exception } } }
Some notes about MyController.java:
  • Specifying @RestController will make this a controller to handle web request. Using @RestController instead of @Controller to perform serialization automatically, without the need of using @ResponseBody
  • The postCustomer is mapped to POST /myapp/customer with the request body as the Customer object
  • The Customer object will be passed on to the RedisService to be stored in Redis, with increasing the hobby count record by 1
  • The getCustomer is mapped to GET /myapp/customer with request param "hobby"
  • Using "hobby" (specific prefix added) as the key to retrieve the count
Customer.java:
package maxjava.model; import lombok.Data; @Data public class Customer { private String name; private String email; private int age; private String hobby; }
Some notes about Customer.java:
  • This is the object used in the request or response body on the Controller APIs
  • Use Lombok @Data for get and set
RedisService.java:
package maxjava.caching; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.PostConstruct; import maxjava.model.Customer; import org.springframework.stereotype.Service; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.Transaction; import java.util.List; @Service public class RedisService { protected JedisPool jedisPool; private final String serverIP = "localhost"; private final int port = 6379; @PostConstruct private void init() { jedisPool = new JedisPool(serverIP, port); } public String getCache(String key) throws Exception { String value = null; try (Jedis jedis = jedisPool.getResource()) { value = jedis.get(key); } return value; } public void setCustomerAndCountHobby(Customer customer) throws Exception { ObjectMapper objectMapper = new ObjectMapper(); String jsonCustomer = objectMapper.writeValueAsString(customer); try (Jedis jedis = jedisPool.getResource()) { String customerKey = "customer:" + customer.getName(); String hobbyKey = "hobby:count:" + customer.getHobby(); jedis.watch(hobbyKey); int currentCount = 0; String value = jedis.get(hobbyKey); if (value != null) { currentCount = Integer.valueOf(value); } int newCount = currentCount + 1; Transaction transaction = jedis.multi(); transaction.set(hobbyKey, String.valueOf(newCount)); transaction.set(customerKey, jsonCustomer); List<Object> response = transaction.exec(); jedis.unwatch(); if (response == null) { // The watched key has been modified by other program, causing this transaction to fail, so throw exception } } } }
Some notes about RedisService.java:
  • During PostConstruct, initiate the JedisPool, which is thread safe to be shared for all requests
  • The setCustomerAndCountHobby function will add a Customer record (for simplicity assuming each request has a different name) and increase the hobby count by 1 in a transaction
  • To use transaction, first call watch() with the key to be watched. Then start the transaction by calling multi() and get the Transaction object
  • Use the Transaction object to apply the business logic (set both Customer and hobby count here)
  • After applying the logic call exec() to execute the transaction
  • If response is not null, that means the watched record has not been changed during the transaction. In this case the transaction is successful. Both Customer and the hobby count are set
  • In another word, if response is null, that means the record has been changed during the transaction, so no update of Customer and hobby will happen
  • The getCache function just retrieve the string (hobby count) from Redis and return
Sample requests and responses:
  • Post a Customer record and update hobby cont:
    curl --request POST '<CONTEXT_PATH>/myapp/customerWithHobby' --header 'Content-Type: application/json' --data-raw ' { "name": "Ethan", "email": "ethan@maxjava.net", "age": 26, "hobby": "Soccer" }'
  • Retrieve the count of hobby "Soccer":
    curl --request GET '<CONTEXT_PATH>/myapp/hobbyCount?hobby=Soccer'
    Response:
    1
  • Post another Customer record and update hobby cont:
    curl --request POST '<CONTEXT_PATH>/myapp/customerWithHobby' --header 'Content-Type: application/json' --data-raw ' { "name": "Johnny", "email": "johnny@maxjava.net", "age": 43, "hobby": "Soccer" }'
  • Retrieve the count of hobby "Soccer" again:
    curl --request GET '<CONTEXT_PATH>/myapp/hobbyCount?hobby=Soccer'
    Response:
    2