Spring Boot + Caching

4 min read

This topic will give you understanding of usage Spring Boot with Caching to increase your app performance significantly 🚀 !

Every one of you — if you already reading this article — faces or has faced with the problem of slow service. Why does it usually happen you would ask me?

Well, there are lots of reasons why your service could work not as fast as you’d expect it to work. Among popular ones:

  • High load — when your service faces big pressure from users
  • Complicated Business Logic — when there are lots of stuff that needs to be computed, calculated during each user request
  • Remote Database — it’s a good practice to have your database hosted by some well-trusted provider, and to be sure it is secured and backed up on a regular basis. But also it impacts the performance, since it requires more time to go from server to the remote database and back with needed data

So as you see, reasons of slowness might be multitudinous.

And here we have a solution for this!

Spring Framework has a really powerful tool to improve your server performance and availability. This tool calls Spring Cache!

So let’s take a closer look at caching data by creating step-by-step server application with Spring Boot using Spring Cache.

Probably the fastest way to create spring boot project is by generating it from Spring InitialzrDependencies that we need are: Web and Cache for now. So download it, unzip and open with you favorite IDE. I prefer IntelliJIdea.

So, when you open it you’ll have the next project structure:

Okay, next we’ll create User domain class + UserService that will fill out dummy data for users + UserController that will define our endpoints.

Our User.class will look like this:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {

private String username;
private int age;
}

Please note annotations above the class name. All they are from lombok library, that you can add to your project using Maven dependency:

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
<scope>provided</scope>
</dependency>

UserController for now will have only one mapping — to fetch all users:

@RestController
@RequestMapping(value = "/users")
public class UserController {

private final UserService userService;

@Autowired
UserController (UserService userService) {
this.userService = userService;
}

@GetMapping(value = "/all")
public List<User> getAllUsers() {
return userService.findAll();
}
}

And UserService, where all magic will take place, for now looks like following:

@Service
public class UserService {

private List<User> users = new ArrayList<>();

@Autowired
UserService() {}

@PostConstruct
private void fillUsers() {
users.add(User.builder().username("user_1").age(20).build());
users.add(User.builder().username("user_2").age(76).build());
users.add(User.builder().username("user_3").age(54).build());
users.add(User.builder().username("user_4").age(30).build());
}

public List<User> findAll() {
simulateSlowService();
return this.users;
}

private void simulateSlowService() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

So, as you see, we have only one endpoint for now — http://localhost:8080/users/all — that should return all users, stored in variable users.

But we are simulating that our service is really slow, by calling method simulateSlowService() — that will just wait 3 seconds before executing logic inside the findAll() method.

And when you execut GET request to our find all users endpoint, you’ll get next result:

So, it takes about 3227 milliseconds to get all users. And this is an horrible scenario for real use case 😏

So here Spring Cache can solve our issue!

Let’s change the code!

Main SpringBootWithCachingApplication.class

@SpringBootApplication
@EnableCaching //enables Spring Caching functionality
public class SpringBootWithCachingApplication {

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

And UserService.class

@Service
@CacheConfig(cacheNames={"users"}) // tells Spring where to store cache for this class
public class UserService {

private List<User> users = new ArrayList<>();

@Autowired
UserService() {}

@PostConstruct
private void fillUsers() {
users.add(User.builder().username("user_1").age(20).build());
users.add(User.builder().username("user_2").age(76).build());
users.add(User.builder().username("user_3").age(54).build());
users.add(User.builder().username("user_4").age(30).build());
}

@Cacheable // caches the result of findAll() method
public List<User> findAll() {
simulateSlowService();
return this.users;
}

private void simulateSlowService() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

@CacheConfig — you can define some of the cache configuration in one place — at the class level — so that you won’t need to declare stuff several times.

@Cacheable — when method annotated with this annotation, it will be executed only once for the given cachekey, until the cache expires or gets cleared.

So, now when you restarted the server and make GET request to fetch all users — first time it will take those 3 with something seconds to get data back to you. Because firstly it will check the cache, and will see that it is empty, and will execute the method, with 3 seconds delay. But all further requests to findAll() will be executed much much faster, because method will not be executed, since requested data was cached previously and will be taken for you from the cache.

As you see, the request execution time is much less, than before. It’s simply because the method findAll() is not executed, since the data is already in cache, that’s why it works so fast 🚀!

Alright! This is good. But not enough. Imagine that some user got update, for example username of first user changed from “user_1” to “user_super_1”. And since method findAll() marked as cacheable, it won’t be executed, and we will get back old data from cache all the time. And this is definitely not something that we want.

So basically, whenever something changes(update, save requests) — we need to properly update our cache. Let’s see how that is possible.

For instance, I’ll add some method that will change first user in array.

@CachePut
public User updateUser(User user) {
this.users.set(0, user);
return this.users.get(0);
}

@CachePut — always lets the method execute. It is used to update the cache with the result of the method execution

So, when we update the first user, and the appropriate method annotated with @CachePut annotation — it will update the cache itself, and the request to findAll() will return us recently updated data. Again, it will be executed very and very fast.

And also there is one more interesting part left:

@CacheEvict — removes data from from the cache. You can use in different ways:

  • if you want to flush all the cache
@CacheEvict(allEntries = true) 

  • Or remove item by key
@CacheEvict(key = "#user.username")

This way you won’t keep data in the cache, that you won’t need to use anymore.

This is basically how Spring Cache can help you in solving performance issue during you development process. But keep in mind not to use caching where it’s not needed. Be wise!

For better understanding of this article you can take a look at my GitHub repository with all the source code available for this topic.

If you want to use some specific tool for caching, you may use Hazelcast, and get more to know by reading this article.

You may check this GitHub account to get sources for current topic and much much more.

Leave a Reply

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