Caching is a critical performance optimization for data-heavy applications. This project implements a multi-layered caching strategy using Hibernate and the high-performance Caffeine caching library via the JCache (JSR-107) standard.
The L2 Cache is shared across all sessions in a SessionFactory.
- Scope: Unlike the L1 cache (which is private to one transaction/session), L2 cache persists data globally for the application.
- Implementation: Enabled in
Employeewith@Cache(usage = CacheConcurrencyStrategy.READ_WRITE). - Behavior: When you fetch an employee by ID, Hibernate checks the L2 cache before hitting the database.
While the L2 cache stores individual entities by ID, the Query Cache stores the results of specific JPQL/HQL queries (effectively a list of IDs).
- Setup: In
EmployeeRepository, we use@QueryHintsto mark specific methods as cacheable. - Workflow:
- Hibernate checks the Query Cache for the list of IDs matching the department.
- If found, it fetches the actual data for those IDs from the L2 cache.
Eviction is the process of manually or automatically removing stale data from the cache.
- The "Force Clear": Our
EmployeeServiceusesemf.getCache().evict(Employee.class, id)to remove a specific employee from the L2 cache, forcing the next request to hit the database.
The CacheConfig bean enables Hibernate's internal performance tracking. Through the /api/cache-stats endpoint, you can see:
- Cache Hits: How many times data was successfully served from memory.
- Cache Misses: How many times Hibernate was forced to go to the database.
- Both use
@Cacheable. EmployeeusesREAD_WRITEbecause employee data often changes.SkillusesREAD_ONLYfor better performance on static reference data.
Unwraps the JPA EntityManagerFactory to access the native Hibernate Statistics API, making performance data available to the REST controller.
Demonstrates using @QueryHints to enable the query cache for complex search methods.
- Initial Load: The
DataLoaderinserts "GeekA" and several skills. - Test Cache Hit:
- GET
/api/employee/1(Initial call: Database hit, L2 Cache population). - GET
/api/employee/1(Subsequent call: Served from L2 Cache).
- GET
- Test Query Cache:
- GET
/api/search/IT(Check Query Cache performance).
- GET
- Test Eviction:
- POST
/api/evict/1(Removes GeekA from cache). - GET
/api/employee/1(Will result in a database hit again).
- POST
- Check Stats:
- GET
/api/cache-statsto verify the hit/miss ratio.
- GET
The application.properties connects Hibernate to Caffeine using the JCacheRegionFactory. This is a modern, high-speed alternative to the older Ehcache or Infinispan providers.
| Concept | Educational Purpose | Logic |
|---|---|---|
| First-Level Cache | Transactional consistency. | Objects are stored for the life of the current Session. |
| Second-Level Cache | Application performance. | Objects are shared across all users in Caffeine RAM. |
| Query Cache | Search speed. | Stores list of IDs from common search criteria. |
| Lazy Loading | Memory efficiency. | Doesn't load "Skills" until you actually try to use them. |
| Eager Loading | Reduced database round-trips. | Fetches "Skills" in the same SQL as the "Employee". |
| Eviction | Data freshness. | Uses algorithms like LRU (Least Recently Used) to keep only the best data in RAM. |
Hibernate:
create table employee (
id bigint not null auto_increment,
department varchar(255),
name varchar(255),
primary key (id)
) engine=InnoDB
Hibernate:
create table employee_skills (
employees_id bigint not null,
skills_id bigint not null,
primary key (employees_id, skills_id)
) engine=InnoDB
Hibernate:
create table skills (
id bigint not null auto_increment,
name varchar(255),
primary key (id)
) engine=InnoDB
Hibernate:
alter table employee_skills
add constraint FK8gyvp36eysxc5o4taxnjahali
foreign key (skills_id)
references skills (id)
Hibernate:
alter table employee_skills
add constraint FKg2w4kdn6q5qe3dnuwssg6akn1
foreign key (employees_id)
references employee (id)
2026-01-17T19:00:16.410+05:30 INFO 13768 --- [Hibernate-Cache-And-Eviction] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2026-01-17T19:00:16.512+05:30 INFO 13768 --- [Hibernate-Cache-And-Eviction] [ main] o.s.d.j.r.query.QueryEnhancerFactories : Hibernate is in classpath; If applicable, HQL parser will be used.
2026-01-17T19:00:16.798+05:30 WARN 13768 --- [Hibernate-Cache-And-Eviction] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2026-01-17T19:00:17.213+05:30 INFO 13768 --- [Hibernate-Cache-And-Eviction] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-01-17T19:00:17.220+05:30 INFO 13768 --- [Hibernate-Cache-And-Eviction] [ main] A.E.HibernateCacheAndEvictionApplication : Started HibernateCacheAndEvictionApplication in 11.511 seconds (process running for 12.265)
Hibernate:
insert
into
skills
(name)
values
(?)
Hibernate:
insert
into
skills
(name)
values
(?)
Hibernate:
insert
into
employee
(department, name)
values
(?, ?)
--- Initial Data Loader ---
Hibernate:
insert
into
employee_skills
(employees_id, skills_id)
values
(?, ?)
Hibernate:
insert
into
employee_skills
(employees_id, skills_id)
values
(?, ?)
2026-01-17T19:05:32.640+05:30 INFO 13768 --- [Hibernate-Cache-And-Eviction] [nio-8080-exec-6] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2026-01-17T19:05:32.734+05:30 INFO 13768 --- [Hibernate-Cache-And-Eviction] [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2026-01-17T19:05:33.364+05:30 INFO 13768 --- [Hibernate-Cache-And-Eviction] [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : Completed initialization in 622 ms
Hibernate:
select
e1_0.id,
e1_0.department,
e1_0.name
from
employee e1_0
where
e1_0.id=?
Hibernate:
select
s1_0.employees_id,
s1_1.id,
s1_1.name
from
employee_skills s1_0
join
skills s1_1
on s1_1.id=s1_0.skills_id
where
s1_0.employees_id=?
Hibernate:
select
e1_0.skills_id,
e1_1.id,
e1_1.department,
e1_1.name
from
employee_skills e1_0
join
employee e1_1
on e1_1.id=e1_0.employees_id
where
e1_0.skills_id=?
Hibernate:
select
s1_0.employees_id,
s1_1.id,
s1_1.name
from
employee_skills s1_0
join
skills s1_1
on s1_1.id=s1_0.skills_id
where
s1_0.employees_id=?
2026-01-17T19:05:41.561+05:30 WARN 13768 --- [Hibernate-Cache-And-Eviction] [nio-8080-exec-6] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: (was java.util.ConcurrentModificationException)]
Hibernate:
select
e1_0.id,
e1_0.department,
e1_0.name
from
employee e1_0
where
e1_0.department=?
Cache Evicted for Employee id: 1 next search will hit database
Hibernate:
select
e1_0.id,
e1_0.department,
e1_0.name
from
employee e1_0
where
e1_0.id=?
GET localhost:8080/api/employee/1
{
"id": 1,
"name": "GeekA",
"department": "IT",
"skills": []
}GET localhost:8080/api/search/IT
[
{
"id": 1,
"name": "GeekA",
"department": "IT",
"skills": []
}
]POST localhost:8080/api/evict/1
Cache cleared for id: 1 Check console for next Search.
GET localhost:8080/api/cache-stats
--- Hibernate Cache Stats ---
Entity Fetch Count: 0
Second Level Cache Hits: 8
Second Level Cache Misses: 4
Query Cache Hits: 2
Query Cache Misses: 1