Spring Boot Transaction-Aware Caching Example

Caching is an essential technique in application development for improving performance and reducing the load on databases and other resources. When working with Spring Framework, we can benefit from transaction-aware caching to ensure data consistency while maintaining the advantages of caching.

In this Spring Boot tutorial, we will explore the concept of Spring’s transaction-aware caching and provide an example of TransactionAwareCacheManagerProxy to illustrate its usage.

1. What is Transaction-Aware Caching?

In Spring, transaction-aware caching is the integration of caching mechanisms with Spring’s transaction management. It ensures that the cache remains consistent with the underlying database during the course of a transaction.

For example, during an operation, if a database transaction is rolled back, the associated cache entries should also be rolled back to their previous state.

Spring supports transaction-aware caching through its caching abstraction and works with various caching providers such as Ehcache, Redis, or Caffeine.

2. Setting Up Transaction-Aware Caching in Spring Boot

Generally, there are two ways to configure transaction-aware caching in a Spring application:

  • First, some of the Spring-provided CacheManager implementations can be configured to use a transactional context. The JCacheCacheManager is one of them. To switch on the transaction awareness, set the transactionAware property to true.
import javax.cache.Caching;
import org.springframework.cache.jcache.JCacheCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CacheConfiguration {
    
    @Bean
    public JCacheCacheManager cacheManager() {

        JCacheCacheManager cacheManager = new JCacheCacheManager();
        cacheManager.setCacheManager(Caching.getCachingProvider().getCacheManager());
        cacheManager.setTransactionAware(true); // Enable transaction awareness
        return cacheManager;
    }
}
  • For others, we can wrap the CacheManager implementation in a TransactionAwareCacheManagerProxy to make them transaction-aware.

    For example, to make Caffeine cache transaction aware, we can define the CacheManager as follows. Doing so will wrap the actual Cache instances with a TransactionAwareCacheDecorator that will register the operations on the cache with the current ongoing transaction (or execute directly if no transaction is available).
@Bean
public CacheManager cacheManager() {

  var caffeine = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(Duration.ofMinutes(5));
  var cacheManager = new CaffeineCacheManager();
  cacheManager.setCaffeine(caffeine);
  return new TransactionAwareCacheManagerProxy(cacheManager);
}

Now when we run our application, everything should still look normal, but all the caching operations are now bound to the successful execution of a transaction. So if a delete operation fails with an exception, the Entity to be deleted would still be in the cache.

3. Transaction-Aware Caching Example

To demonstrate the caching that is transaction-aware, we are extending the Caffeine cache example.

The following StudentService class has @Transactional annotation and performs all operations in transactional boundaries.

import com.howtodoinjava.dao.StudentRepository;
import com.howtodoinjava.exception.RecordNotFoundException;
import com.howtodoinjava.model.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class StudentService {

  @Autowired
  private StudentRepository studentRepository;

  @Cacheable("StudentCache")
  public Student getById(Long id) {

    return studentRepository.findById(id)
        .orElseThrow(() -> new RecordNotFoundException("Record not wound with id: " + id));
  }

  @CacheEvict(value = "StudentCache", key = "#id")
  public void deleteById(Long id) {

    //TODO: REMOVE IT. Its for Demo Only.
    if (id == 1) {
      // Simulate a failure scenario where student id is 1 and cannot be deleted.
      throw new RuntimeException("Student cannot be deleted. It has magic ID");
    }
    studentRepository.deleteById(id);
  }

  @CacheEvict(value = "StudentCache", key = "#student.id")
  public Student save(Student student) {
    return studentRepository.save(student);
  }
}

To demonstrate the transaction aware caching, we have added a condition that forcefully throws a RuntimeException when the student id is 1.

Additionally, we have added @EnableCaching and @EnableTransactionManagement in a @Configuration class to enable the caching and transaction management, respectively.

import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.transaction.TransactionAwareCacheManagerProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableCaching
@Configuration
@EnableTransactionManagement
public class CacheConfig {

  @Bean
  public Caffeine<Object, Object> caffeineConfig() {
    return Caffeine.newBuilder()
        .expireAfterWrite(300, TimeUnit.SECONDS)
        .initialCapacity(10);
  }

  @Bean
  public CacheManager cacheManager(Caffeine caffeine) {
    var caffeineCacheManager = new CaffeineCacheManager();
    caffeineCacheManager.setCaffeine(caffeine);
    return new TransactionAwareCacheManagerProxy(caffeineCacheManager);
  }
}

Finally, we run a few database operations to test if the entry is removed from the cache or not.

import com.howtodoinjava.dao.StudentRepository;
import com.howtodoinjava.model.Student;
import com.howtodoinjava.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App implements CommandLineRunner {

  @Autowired
  StudentService studentService;

  public static void main(String[] args) {

    SpringApplication.run(App.class, args);
  }

  @Override
  public void run(String... args) throws Exception {

    Student student = studentService.save(new Student("Lokesh Gupta"));

    student = studentService.getById(student.getId()); //Hits the database
    System.out.println(student);
    student = studentService.getById(student.getId()); //Fetched from cache
    System.out.println(student);

    try {
      studentService.deleteById(1L);
    } catch (Exception e) {
      System.out.println(e.getMessage());
    }

    student = studentService.getById(student.getId()); //Fetched from cache
    System.out.println(student);
  }
}

Notice the program output that after the delete operation, when we fetch the Student entity then no database query is executed and the result is fetched from the cache.

Hibernate: insert into student (name,id) values (?,?)
Hibernate: select s1_0.id,s1_0.name from student s1_0 where s1_0.id=?
Student(id=1, name=Lokesh Gupta)		# From Database
Student(id=1, name=Lokesh Gupta)		# From Cache
Student cannot be deleted. It has magic ID
Student(id=1, name=Lokesh Gupta)		# From Cache

4. Conclusion

Transaction-aware caching is a powerful feature that ensures data consistency between the cache and the database during transactional operations. It is extremely beneficial when dealing with applications that require high data integrity and performance.

In this article, we explored transaction-aware caching in the context of a Spring Boot applications with the help of TransactionAwareCacheManagerProxy class.

Happy Learning !!

Source Code on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode