Implementing Caching in Spring REST Services

Introduction

Here I am going to show an example on implementing caching in Spring REST web services. Caching is used to store copies of frequently accessed data in several places for the request-response path. In other words, it stores a copy of a given resource and serves it back when requested.

The performance of web sites and applications can be significantly improved by reusing previously fetched resources. Caching in REST services reduce latency and network traffic and thus lessen the time needed to display a representation of a resource. By making use of HTTP caching, REST APIs become more responsive.

There are several kinds of caches but these can be grouped into two main categories – private or shared caches. A shared cache is a cache that stores responses for reuse by more than one user. A private cache is dedicated to a single user.

Prerequisites

Java at least 1.8, Spring Boot 2.4.2, Gradle 6.7.1, Maven 3.6.3

Project Setup

Create a gradle or maven based project in your favorite tool or IDE, the project name you can give as spring-boot-rest-caching.

In the below build script I have included spring boot web, spring boot data jpa and h2 database. The in-memory h2 database is very useful when you need to do a quick PoC because it saves you a lots of time and effort required to setup a database.

For the maven based project you can use the following pom.xml file.

<?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>com.roytuts</groupId>
	<artifactId>spring-rest-cache</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>12</maven.compiler.source>
		<maven.compiler.target>12</maven.compiler.target>
	</properties>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.2</version>
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

If you are creating gradle based project. update the build.gradle file with the below content to include required dependencies.

buildscript {
	ext {
		springBootVersion = '2.4.2'
	}
	
    repositories {
    	maven {
    		url 'https://plugins.gradle.org/m2/'
    	}
    }
    
    dependencies {
    	classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

plugins {
    id 'java-library'
    id 'org.springframework.boot' version "${springBootVersion}"
}

sourceCompatibility = 12
targetCompatibility = 12

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
	implementation("org.springframework.boot:spring-boot-starter-cache:${springBootVersion}")
	implementation("org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}")
	implementation 'com.h2database:h2:1.4.200'
}

Entity Class

This application needs database to perform database operations. Therefore I need to create entity class to map out database column with Java class attributes.

The beauty of Spring Boot Data JPA starter is the below class will create the required table in the database. Even if you don’t mention any column name, then also it will create same column name in the table as the Java attributes.

package com.roytuts.spring.boot.rest.caching.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	private String name;
	private String address;
	public User() {
	}
	public User(String name, String address) {
		this.name = name;
		this.address = address;
	}
	//getters and setters
}

Repository Interface

Spring Data JPA provides built-in methods for performing basic CRUD operations and you don’t need to write any query method.

Another beauty of Spring Data JPA is that you write Java method to query your database and Spring generates the corresponding query from your method for performing database operations.

Related Posts:

I have just extended the JpaRepository interface but I have just written one query method inside this repository to fetch user by name, but I will use Spring’s built-in methods for performing other operations.

package com.roytuts.spring.boot.rest.caching.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.roytuts.spring.boot.rest.caching.entity.User;
public interface UserRepository extends JpaRepository<User, Integer> {
	User findByName(String name);
}

Service Class

The service class is used generally if you want to perform some business logic or conversion to different data.

We are fetching user data on different conditions in this service class.

package com.roytuts.spring.boot.rest.caching.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.roytuts.spring.boot.rest.caching.entity.User;
import com.roytuts.spring.boot.rest.caching.repository.UserRepository;

@Service
public class UserService {

	@Autowired
	private UserRepository repository;

	public List<User> getUserList() {
		return repository.findAll();
	}

	public User getUserById(int id) {
		return repository.findById(id).get();
	}

	public User getUserByName(String name) {
		return repository.findByName(name);
	}

}

REST Controller

Now I will expose REST APIs so that any client can call these REST services and retrieve user data.

Exposing the operations on REST makes loosely coupled to integrate with any client technologies.

package com.roytuts.spring.boot.rest.caching.rest.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.roytuts.spring.boot.rest.caching.entity.User;
import com.roytuts.spring.boot.rest.caching.service.UserService;

@RestController
public class UserRestController {

	@Autowired
	private UserService service;

	@GetMapping("/users")
	public ResponseEntity<List<User>> getUserList() {
		return new ResponseEntity<List<User>>(service.getUserList(), HttpStatus.OK);
	}

	@GetMapping("/user/id/{id}")
	public ResponseEntity<User> getUserById(@PathVariable int id) {
		return new ResponseEntity<User>(service.getUserById(id), HttpStatus.OK);
	}

	@GetMapping("/user/name/{name}")
	public ResponseEntity<User> getUserByName(@PathVariable String name) {
		return new ResponseEntity<User>(service.getUserByName(name), HttpStatus.OK);
	}

}

Application Properties

I will create an application.properties file that will hold some configurations. You can also create application.yml file. This file should be put under src/main/resources folder.

The below properties file contains SQL format and h2 database console enablement to see the queries executed during performing of the database operations.

spring.jpa.show-sql=true
spring.h2.console.enabled=true

Dumping Data into Table

As I am not creating any new record through REST API and I need some data to test the application, so I will insert some data during application startup.

Therefore create below SQL insert script and put it under src/main/resources folder.

insert into user(name,address)
values('Soumitra','Earth');
insert into user(name,address)
values('Liton','Mars');
insert into user(name,address)
values('Suman','Jupiter');
insert into user(name,address)
values('Debabrata','Neptune');

Main Class

Creating a main method in a class with @SpringBootApplication annotation would be sufficient to deploy the application into the embedded Tomcat server. This is a great advantage that you just need to let Spring know that it is your Spring Boot Application using @SpringBootApplication and main class.

You also need to tell Spring where your entity class and repository interface.

package com.roytuts.spring.boot.rest.caching;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EntityScan("com.roytuts.spring.boot.rest.caching.entity")
@EnableJpaRepositories("com.roytuts.spring.boot.rest.caching.repository")
@SpringBootApplication(scanBasePackages = "com.roytuts.spring.boot.rest.caching")
public class SpringRestCacheApp {

	public static void main(String[] args) {
		SpringApplication.run(SpringRestCacheApp.class, args);
	}

}

Testing the Application

Now if you run the above main class and test your REST APIs then you will see every time you hit the REST URL, your data will be fetched from database. Therefore on every request through REST API database gets hit.

For example, you may try these below requests:

Fetching All Users

Request – GET http://localhost:8080/users 2 times

Response

[
    {
        "id": 1,
        "name": "Soumitra",
        "address": "Earth"
    },
    {
        "id": 2,
        "name": "Liton",
        "address": "Mars"
    },
    {
        "id": 3,
        "name": "Suman",
        "address": "Jupiter"
    },
    {
        "id": 4,
        "name": "Debabrata",
        "address": "Neptune"
    }
]

Console Output

Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_

Fetching User by ID

Request – GET http://localhost:8080/user/id/1 3 times

Response

{
	"id": 1,
	"name": "Soumitra",
	"address": "Earth"
}

Console Output

Hibernate: select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?

Fetching User by Name

Request – GET http://localhost:8080/user/name/Soumitra 2 times

Response

{
    "id": 1,
    "name": "Soumitra",
    "address": "Earth"
}

Console Output

Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?

So you have seen in the above outputs without using caching in Spring REST services and that’s why every time you request the REST API, the database was called.

Now I will implement caching in REST services and you will see no matter how many times I call the REST APIs, the database will be hit only first time to fetch data from database and put into cache. Next time onward for each same request data will be fetch from cache.

Implementing Caching

Now I will implement caching in Spring REST services. So I will update the existing service class and main class I had created in the above sections.

Update Service Class

Update the service class as follows to implement caching for your public methods.

You need to put @Cacheable annotation with name so that Spring will find data into cache easily by cache name.

package com.roytuts.spring.boot.rest.caching.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.roytuts.spring.boot.rest.caching.entity.User;
import com.roytuts.spring.boot.rest.caching.repository.UserRepository;
@Service
public class UserService {
	@Autowired
	private UserRepository repository;
	@Cacheable("users")
	public List<User> getUserList() {
		return repository.findAll();
	}
	@Cacheable("user-id")
	public User getUserById(int id) {
		return repository.findById(id).get();
	}
	@Cacheable("user-name")
	public User getUserByName(String name) {
		return repository.findByName(name);
	}
}

Updating Main Class

Update the main class to enable the processing of the caching annotations.

The @EnableCaching annotation triggers a post processor that inspects every Spring bean for the presence of caching annotations on public methods. If such an annotation is found, a proxy is automatically created to intercept the method call and handle the caching behavior accordingly.

package com.roytuts.spring.boot.rest.caching.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableCaching
@SpringBootApplication(scanBasePackages = "com.roytuts.spring.boot.rest.caching")
@EntityScan("com.roytuts.spring.boot.rest.caching.entity")
@EnableJpaRepositories("com.roytuts.spring.boot.rest.caching.repository")
public class SpringRestCachingApp {
	public static void main(String[] args) {
		SpringApplication.run(SpringRestCachingApp.class, args);
	}
}

Testing the Application

Now call the REST APIs to fetch data from database. You will find only first time database is hit and for subsequent requests the data are fetched from caching no matter how many times you invoke the REST services.

You will find only one database hit per each REST service call in the console output.

Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_
Hibernate: select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?

Hope you got an idea how to implement cache in REST web services using Spring Boot framework.

Source Code

Download

Leave a Reply

Your email address will not be published. Required fields are marked *