🤯 @Cacheable vs First Level Cache vs Second Level Cache — Explained
If you’re learning Spring Boot + Hibernate, chances are you’ve asked this:
❓ If I don’t use @Cacheable, will my request still go to L2 cache? ❓ What is the real difference between L1, L2 cache and @Cacheable?
I was confused too — until I understood WHERE each cache actually works.
Let me explain this once and for all 👇
🧠 Big Picture (THIS is the key)
There are three different caches, working at three different layers:
Spring Service Layer → @Cacheable Hibernate ORM Layer → First Level Cache (L1) Hibernate ORM Layer → Second Level Cache (L2)
👉 They are independent, not replacements for each other.
1️⃣ First Level Cache (L1 Cache)
What it is Hibernate’s internal cache
Works inside one transaction
Enabled by default (cannot be disabled)
Example
@Transactional
public void method() {
userRepo.findById(1); // DB hit
userRepo.findById(1); // L1 cache hit
}
Important Lives only till the transaction ends
Cleared automatically after method ends
👉 L1 cache avoids duplicate DB queries inside the same request
2️⃣ Second Level Cache (L2 Cache)
What it is Shared cache at application level
Works across multiple requests
Must be explicitly enabled
Implemented using cache providers (Ehcache, Redis, Hazelcast, etc.)
🔧 NOTE: Redis is OPTIONAL here. Hibernate L2 can work with in-memory providers like Ehcache or Caffeine.
Flow:
Request 1 → DB hit → Data stored in L2
Request 2 → L2 cache hit → No DB call
Request 3 → L2 cache hit → No DB call
Key points:
Method still executes
Hibernate is still involved
Only the database call is avoided
👉 L2 cache avoids repeated DB queries across requests
3️⃣ @Cacheable (Spring Cache)
This is where most confusion happens.
What @Cacheable actually does Works at service method level
Caches the method result
If cache hits → method is NOT executed at all
Example
@Cacheable("users")
public User getUser(Long id) {
return userRepo.findById(id).get();
}
Flow:
Cache hit → method skipped → Hibernate not touched → DB not touched
👉 This is the fastest cache because it bypasses Hibernate completely.
❓ The BIG QUESTION
If I don’t use @Cacheable, will my request go to L2 cache?
✅ YES — absolutely!
Even without @Cacheable, Hibernate will still check:
L1 Cache → L2 Cache → Database
@Cacheable is optional and works at a higher layer (Spring).
🔥 Final Decision Flow (MEMORIZE THIS)
Is @Cacheable present?
|
|-- YES → Cache hit → RETURN (Hibernate not used)
|
|-- NO → Hibernate checks
|
|-- L1 cache
|-- L2 cache
|-- Database
🧩 One-line summaries (Save these 🧠)
L1 Cache → Transaction-level memory
L2 Cache → Application-level memory (Hibernate)
@Cacheable → Method-level memory (Spring)
🎯 When to use what?
✅ L1 cache → Always (automatic)
✅ L2 cache → Read-heavy, rarely changing data
✅ @Cacheable → Expensive methods, heavy DB joins, external API calls
🎯 Final mental model
L1 → one transaction
L2 → whole app (Hibernate)
@Cacheable → whole app (Spring)
IMPLEMENTATION ------------------------------
1. First Level Cache
2. @Cacheable with Redis
3. @Cacheable without Redis
4. Second Level Cache with Hibernate
1️⃣ First Level Cache — Nothing to Implement
You DO NOT implement L1 cache. Hibernate gives it automatically.
What you must have:
✔ Spring Data JPA
✔ @Transactional
Example
@Service
public class UserService {
@Transactional
public User testL1Cache(Long id) {
User u1 = userRepository.findById(id).get(); // DB hit
User u2 = userRepository.findById(id).get(); // L1 cache hit
return u1;
}
}
Lifecycle:
Transaction starts → Hibernate Session created → L1 cache created → Transaction ends → L1 cache destroyed ❌
2️⃣ @Cacheable with Redis (Spring Cache – Most Common)
❌ Redis + @Cacheable is NOT Hibernate L2 cache
Redis + Hibernate L2 cache → different setup (Hibernate JCache)
Redis + @Cacheable → Spring Cache
In most real projects:
Redis with @Cacheable used.
Do NOT use Hibernate L2 unless required
Recommended by LinkedIn
Run Redis (required)
Using Docker (OPTIONAL but convenient)
docker run -d --name redis -p 6379:6379 redis
🔧 NOTE: Docker is NOT mandatory. Redis just needs to be running somewhere.
Redis running on: localhost:6379
Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Enable caching
@SpringBootApplication
@EnableCaching
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
Configuration
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.cache.redis.time-to-live=600000
Usage
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
System.out.println("DB HIT");
return userRepository.findById(id).get();
}
}
What happens:
Call 1 → cache MISS → DB hit → cached Call 2 → cache HIT → method NOT executed
✔ Hibernate not involved ✔ DB not involved ✔ Shared across users
🧠 Final takeaway
L1 cache is automatic and short-lived, L2 cache is Hibernate’s shared memory, and @Cacheable is Spring’s method-level cache that can completely bypass Hibernate.
Q. can we use @Cacheable without Redis or any external cache?
✅ Short answer Yes. @Cacheable works even without Redis, Ehcache, or any other cache provider.
Spring will use an in-memory cache by default.
🧠 What Spring uses by default
If you add only this dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
and enable caching:
@EnableCaching
👉 Spring Boot automatically uses ConcurrentMapCacheManager.
That means:
Cache is stored in application memory
Backed by ConcurrentHashMap
No Redis. No Ehcache. No config.
🧪 Example (NO Redis)
@Service
public class UserService {
@Cacheable("users")
public User getUser(Long id) {
System.out.println("DB HIT");
return userRepository.findById(id).get();
}
}
What happens:
First call → DB HIT Second call → no DB HIT
✔ Method skipped ✔ Cache hit ✔ In-memory cache used
🟢 When this is OK
✔ Single application instance
✔ Low to medium traffic
✔ Local development
✔ Small datasets
✔ No restart concerns
🔴 When this is NOT OK
❌ Multiple app instances ❌ Frequent restarts ❌ Large cache size ❌ Need cache sharing
Because each instance has its own cache.
🔵 Hibernate Second Level Cache (L2) with Redis
✅ Redis stores Hibernate entities
✅ Hibernate decides cache hits
1️⃣ What Hibernate L2 Cache with Redis does
Stores entity data in Redis
Shared across all sessions & users
Checked before DB
Works across multiple requests
What it does NOT do:
❌ Does NOT skip method execution ❌ Does NOT skip Hibernate ❌ Does NOT cache method results
👉 It only avoids DB calls
2️⃣ Run Redis (required)
Using Docker (OPTIONAL)
docker run -d --name redis -p 6379:6379 redis
3️⃣ Add dependencies
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jcache</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-hibernate-6</artifactId>
<version>3.25.2</version>
</dependency>
4️⃣ Enable Hibernate L2 cache
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.use_query_cache=false
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.jcache.JCacheRegionFactory
spring.jpa.properties.hibernate.javax.cache.provider=org.redisson.jcache.JCachingProvider
spring.jpa.properties.hibernate.javax.cache.uri=classpath:redisson.yaml
5️⃣ Redisson configuration
singleServerConfig:
address: "redis://127.0.0.1:6379"
threads: 4
nettyThreads: 4
6️⃣ Mark ENTITY as cacheable
@Entity
@Cacheable
@org.hibernate.annotations.Cache(
usage = CacheConcurrencyStrategy.READ_ONLY
)
public class User {
@Id
private Long id;
private String name;
}
7️⃣ Use repository normally
public User getUser(Long id) {
return userRepository.findById(id).get();
}
Hibernate handles everything automatically.
8️⃣ Request flow
Request 1 → L1 miss → L2 miss → DB HIT → Entity stored in Redis (L2)
Request 2 → L1 miss → L2 HIT → DB NOT called
✔ Hibernate executed ✔ Redis used ❌ DB skipped
Hope you find it hopeful.