How I Improved Application Performance: Hibernate Caching in Spring Boot

The Java Trail
8 min readSep 30, 2023

--

In the world of software development, performance is a top priority. Whether you’re building a small web application or a large-scale enterprise system, users expect fast and responsive software. As developers, we constantly strive to optimize our code and reduce those dreaded load times.

One of the critical factors affecting application performance is database interaction. Databases are the backbone of many applications, storing and retrieving data efficiently is crucial. This is where Hibernate, a popular Object-Relational Mapping (ORM) framework, comes into play. Hibernate simplifies the process of interacting with databases by mapping database tables to Java objects. It’s a powerful tool, but like any tool, it needs to be used wisely.

In this article, I’ll take you on a journey through my experience of improving database performance in a Spring Boot application using Hibernate’s second-level caching. We’ll delve into the world of caching strategies, configuration settings, and best practices to boost your application’s responsiveness. By the end of this journey, you’ll have the knowledge and tools to supercharge your database performance.

Caching

ORM frameworks like Hibernate provide a convenient way to work with databases by mapping database tables to Java objects. They cache data transparently. Caching means storing frequently accessed data in memory, which reduces the need to query the database repeatedly. This can lead to significant performance improvements, especially when dealing with large object graphs.

A second-level cache in Hibernate is a shared cache that operates at the session factory level, making it accessible across multiple sessions. It is used to store and manage entity data so that it can be efficiently retrieved without having to hit the database repeatedly. Here’s a detailed explanation with an example:

Second-Level Cache Explanation:

  1. First-Level Cache: In Hibernate, each session (database transaction) has its own first-level cache. This cache is used to store and manage entity instances that are retrieved or manipulated within that session. The first-level cache is isolated and bound to a specific session. When the session is closed, the first-level cache is discarded.
  2. Second-Level Cache: In contrast, the second-level cache is a global cache shared among all sessions created from the same session factory. It operates at a higher level, allowing data to be cached and retrieved across different sessions and transactions.

How Second-Level Cache Works:

When an entity instance is looked up by its ID (primary key), and second-level caching is enabled for that entity, Hibernate follows these steps:

  1. Check First-Level Cache: Hibernate first checks the first-level cache (session cache) associated with the current session. If the entity instance is already present in the first-level cache, it is immediately returned, avoiding a database query.
  2. Check Second-Level Cache: If the entity is not found in the first-level cache, Hibernate checks the second-level cache. The second-level cache stores entity data at a global level, making it accessible to all sessions.
  3. Load from Database: If the entity data is not found in either the first-level cache or the second-level cache, Hibernate proceeds to load the data from the database. Once the data is fetched from the database, Hibernate assembles an entity instance and stores it in the first-level cache for the current session.
  4. Caching for Subsequent Calls: Once an entity instance is in the first-level cache (session cache), it is returned for all subsequent calls within the same session without the need for additional database queries. Additionally, if the entity data was fetched from the database, it may also be stored in the second-level cache for future use by other sessions.

Region Factor:

Region Factory serves as a bridge or intermediary between Hibernate and specific cache providers. Its primary responsibility is to abstract the underlying cache provider’s details and provide a standard interface for Hibernate to interact with different caching solutions.

<property name="hibernate.cache.use_second_level_cache" value="true" />
<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory" />

By providing the Ehcache Region Factory, you instruct Hibernate to use Ehcache as the underlying caching mechanism.

Concurrent Caching Strategy:

CacheConcurrencyStrategy.READ_ONLY:

  • This strategy is suitable for entities that are read frequently but rarely modified. It assumes that the data is mostly static and does not change frequently.
  • No automatic synchronization with the database when entities are updated or deleted.

CacheConcurrencyStrategy.READ_WRITE:

  • This strategy is used for entities that are frequently both read and modified. It ensures that cached data is kept up-to-date with changes in the database.
  • Automatic synchronization with the database when entities are updated or deleted.
  • This strategy guarantees strong consistency which it achieves by using ‘soft’ locks: When a cached entity is updated, a soft lock is stored in the cache for that entity as well, which is released after the transaction is committed. All concurrent transactions that access soft-locked entries will fetch the corresponding data directly from database.
  • Suitable for entities that are updated occasionally but read frequently.

In JTA Environment: If your application uses JTA for managing transactions (common in enterprise-level applications), you need to specify the property hibernate.transaction.manager_lookup_class. This property should specify a strategy for obtaining the JTA TransactionManager, which is crucial for managing distributed transactions in JTA environments.

Non-JTA Environment: you should ensure that the transaction is completed properly when you call Session.close() or Session.disconnect(). Proper transaction management is essential to maintain data consistency when using caching.

CacheConcurrencyStrategy.NONSTRICT_READ_WRITE:

  • This strategy allows for better performance than READ_WRITE while sacrificing some cache consistency.
  • Suitable for entities that are updated less frequently than they are read and where slight data staleness is acceptable.
  • Automatic synchronization with the database when entities are updated or deleted but with less strict consistency guarantees compared to READ_WRITE.
  • Provides improved write performance compared to READ_WRITE because it doesn't acquire locks during read operations.

How to use Second Level Caching Using Spring Boot

Step 1: Set Up a Spring Boot Project

You can create a new Spring Boot project using Spring Initializr or your preferred IDE. Ensure you select the required dependencies for Spring Boot, Spring Data JPA, Hibernate, Ehcache, and Web as a minimum.

Step 2: Configure Ehcache

Create an Ehcache configuration file ehcache.xml in the src/main/resources directory. Here's a simple example:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
updateCheck="true" monitoring="autodetect" dynamicConfig="true">
<defaultCache
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="7200"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU" />

<!-- Define specific cache regions for entities here -->
</ehcache>
  • timeToIdleSeconds: The maximum amount of time (in seconds) that an entry can be idle in the cache (not accessed) before it is considered expired and removed.
  • maxEntriesLocalHeap: The maximum number of cache entries to be stored in the local heap memory. When this limit is reached, older entries are evicted to make space for new ones.
  • timeToLiveSeconds: The maximum amount of time (in seconds) that an entry can exist in the cache before it is considered expired and removed, regardless of whether it has been accessed or not.
  • diskPersistent: If true, disk storage is persistent, meaning entries are retained even after a system restart. If false, entries are lost on system restart.

Step 3: Entity Class

Create an entity that you want to cache. For example, a Product entity:

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
// Constructors, getters, and setters
}

Step 4: Repository Interface

import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Define custom query methods if needed
}

Step 5: Service Class

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
public Product getProductById(Long productId) {
// The following query result will be cached if caching is configured properly
return productRepository.findById(productId).orElse(null);
}
// Other service methods
}

Step 6: Configure Application Properties

In your application.properties ensure you have the necessary properties to enable caching and specify the Ehcache configuration:

# Enable Hibernate second-level cache
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
# Specify the region factory class for Ehcache
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory
# Ehcache configuration file location
spring.cache.ehcache.config=classpath:ehcache.xml

When you call productRepository.findById(productId) in your ProductService, Hibernate and Spring Data JPA handle the session and cache management. Here's what happens:

  • If the requested entity (e.g., Product) is found in the second-level cache, it is returned from the cache, and no database session is opened.
  • If the entity is not found in the cache, a database session is automatically opened by Spring Data JPA. The database query is executed to retrieve the entity.
  • The retrieved entity is stored in the second-level cache and returned to your service method.

Caching Collections (One-Many & Many-Many Relations)

Collection caching allows you to cache entire collections of associated entities. These collections can be part of your domain model, such as one-to-many or many-to-many relationships between entities.

Collection caching is valuable when dealing with associations between entities that are frequently loaded and where caching can lead to significant performance gains. When you enable collection caching, Hibernate caches entire collections, such as lists or sets, associated with an entity.

When Hibernate caches a collection, it doesn’t cache the entire collection of entities but rather caches the IDs of the entities contained in the collection.

  • Caching only the IDs reduces memory usage compared to caching the entire collection of entities.
  • When a collection is updated, only the relevant IDs need to be invalidated in the cache, rather than the entire collection. This minimizes cache invalidation overhead.
@Entity
@Cacheable
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
@OneToMany(mappedBy = "category")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
private List<Product> products;
// Getters and setters
}
@Entity
@Cacheable
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
@ManyToOne
private Category category;
// Getters and setters
}
  • Hibernate creates a cache region named com.example.model.Category.products. This region is dedicated to storing cached collections of products associated with categories.
  • Let’s say we have a category with ID 1 and it has products with IDs 101, 102, and 103. Hibernate stores this data in the cache using a key-value pair.
  • The key-value pair might look like "Category:1:products — [101, 102, 103]"

What happens when Service Layer this method is called?

// Fetch products for category 1
List<Product> products = categoryService.getProductsForCategory(1);

The first time you request products for a category (e.g., category 1), Hibernate loads the products from the database and stores them in the second-level cache.

On subsequent requests -

  • It checks the cache region for that category’s products using the key "Category:1:products".
  • If the key exists in the cache, it retrieves the serialized list of product IDs (e.g., [101, 102, 103]).
  • Using these product IDs, Hibernate efficiently retrieves the corresponding product entities from the second-level cache or the database.

--

--

The Java Trail

Scalable Distributed System, Backend Performance Optimization, Java Enthusiast. (mazumder.dip.auvi@gmail.com Or, +8801741240520)