🤯 @Cacheable vs First Level Cache vs Second Level Cache — Explained

🤯 @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


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.

To view or add a comment, sign in

More articles by Saumya Garg

Others also viewed

Explore content categories