Why Local Caffeine/Guava Caches Outperform Redis in High‑Concurrency SpringBoot Apps

SpringBoot developers can dramatically boost throughput and cut latency by pairing a microsecond‑level local cache (Caffeine or Guava) with Redis, using a two‑level architecture that isolates hot data in JVM memory, reduces network and serialization overhead, and provides configurable eviction policies for various use cases.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Why Local Caffeine/Guava Caches Outperform Redis in High‑Concurrency SpringBoot Apps

In high‑concurrency interface optimization, many developers over‑rely on Redis distributed cache, ignoring that JVM‑resident local caches can serve data in microseconds, eliminating network I/O, serialization and protocol overhead.

The standard two‑level cache architecture consists of a local cache (Caffeine or Guava) plus Redis. The local cache absorbs high‑frequency hot traffic, reduces pressure on Redis; Redis guarantees data sharing and consistency across instances.

What is a local cache? It stores hot data directly in JVM memory, without any middleware, network cost, or connection‑pool consumption, making it the fastest cache form. Its characteristics are strong isolation, extreme speed, inability to share across instances, and a brief window of data inconsistency.

Applicable scenarios include read‑heavy/write‑light workloads, tolerance for short‑term inconsistency, high‑frequency access, and static reference data such as system dictionaries, enums, province‑city data, configuration flags, homepage hot data, short‑lived temporary cache, and the first layer of a two‑level cache.

Java local‑cache options :

ConcurrentHashMap – simplest, no expiration or eviction, easy OOM, rarely used in production.

Guava Cache – mature, LRU‑based eviction, default for legacy projects.

Caffeine Cache – next‑gen, based on W‑TinyLFU algorithm, superior hit rate, throughput and memory utilization; recommended for new projects.

Eviction algorithm differences :

Guava uses LRU – evicts the least‑recently‑used entry, vulnerable to one‑time traffic pollution.

Caffeine uses W‑TinyLFU – combines access frequency and recency, intelligently identifies hot data, resists cache pollution, and achieves industry‑leading hit rates.

Guava Cache example (Maven dependencies and configuration):

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>32.1.3-jre</version>
</dependency>

Configuration class:

import com.google.common.cache.CacheBuilder;
import org.springframework.cache.CacheManager;
import org.springframework.cache.guava.GuavaCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.cache.annotation.EnableCaching;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class GuavaCacheConfig {
    @Bean("guavaCacheManager")
    public CacheManager guavaCacheManager() {
        GuavaCacheManager cacheManager = new GuavaCacheManager();
        // Global default: 15 min TTL, max 10 000 entries, removal listener
        CacheBuilder<Object, Object> globalBuilder = CacheBuilder.newBuilder()
                .expireAfterWrite(15, TimeUnit.MINUTES)
                .maximumSize(10000)
                .removalListener(notification -> {
                    System.out.printf("[Guava eviction] key=%s, cause=%s%n",
                            notification.getKey(), notification.getCause());
                });
        cacheManager.setCacheBuilder(globalBuilder);
        // Per‑group custom TTL/capacity
        Map<String, CacheBuilder<Object, Object>> customConfig = Map.of(
                "shortTempCache", CacheBuilder.newBuilder()
                        .expireAfterWrite(5, TimeUnit.MINUTES)
                        .maximumSize(2000),
                "baseDictCache", CacheBuilder.newBuilder()
                        .expireAfterWrite(24, TimeUnit.HOURS)
                        .maximumSize(5000)
        );
        cacheManager.setCustomCacheConfigurations(customConfig);
        return cacheManager;
    }
}

Annotation‑driven usage (RestController):

@RestController
public class GuavaCacheController {
    // Long‑term dictionary cache
    @Cacheable(value = "baseDictCache", key = "#dictCode", cacheManager = "guavaCacheManager")
    @GetMapping("/dict/guava/{dictCode}")
    public String getDict(@PathVariable String dictCode) {
        System.out.println("Query DB for dict: " + dictCode);
        return dictCode + " dictionary data (Guava long‑term cache)";
    }

    // Short‑term temporary cache
    @Cacheable(value = "shortTempCache", key = "#param", cacheManager = "guavaCacheManager")
    @GetMapping("/temp/guava/{param}")
    public String tempCache(@PathVariable String param) {
        System.out.println("Query temporary business data");
        return "Temporary data: " + param;
    }

    // Update and evict
    @CacheEvict(value = "baseDictCache", key = "#dictCode", cacheManager = "guavaCacheManager")
    @GetMapping("/dict/update/{dictCode}")
    public String updateDict(@PathVariable String dictCode) {
        System.out.println("Update DB dictionary data");
        return "Update successful, local cache cleared";
    }
}

Caffeine Cache example (Maven dependencies and configuration):

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
  <groupId>com.github.benmanes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
  <version>2.9.3</version>
</dependency>

Configuration class:

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

@Configuration
@EnableCaching
public class CaffeineCacheConfig {
    @Bean("caffeineCacheManager")
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        // Global default: 20 min TTL, max 20 000 entries, removal listener
        Caffeine<Object, Object> globalCaffeine = Caffeine.newBuilder()
                .expireAfterWrite(20, TimeUnit.MINUTES)
                .maximumSize(20000)
                .removalListener((key, value, cause) ->
                        System.out.printf("[Caffeine eviction] key=%s, cause=%s%n", key, cause));
        cacheManager.setCaffeine(globalCaffeine);
        cacheManager.setAllowNullValues(true);
        // Custom group specs
        Map<String, Caffeine<Object, Object>> customCache = Map.of(
                "hotGoodsLocal", Caffeine.newBuilder()
                        .expireAfterWrite(1, TimeUnit.HOURS)
                        .maximumSize(8000),
                "systemConfigLocal", Caffeine.newBuilder()
                        .expireAfterWrite(12, TimeUnit.HOURS)
                        .maximumSize(3000)
        );
        cacheManager.setCustomCaffeineSpecs(customCache);
        return cacheManager;
    }
}

Annotation‑driven usage (RestController):

@RestController
public class CaffeineCacheController {
    @Cacheable(value = "hotGoodsLocal", key = "#id", cacheManager = "caffeineCacheManager")
    @GetMapping("/goods/local/{id}")
    public String getGoodsInfo(@PathVariable Long id) {
        System.out.println("Query DB for goods: " + id);
        return "Hot product data (Caffeine high‑performance cache)";
    }

    @Cacheable(value = "systemConfigLocal", key = "#key", cacheManager = "caffeineCacheManager")
    @GetMapping("/config/local/{key}")
    public String getConfig(@PathVariable String key) {
        System.out.println("Query system config");
        return "System config: " + key;
    }

    @CacheEvict(value = "systemConfigLocal", key = "#key", cacheManager = "caffeineCacheManager")
    @GetMapping("/config/refresh/{key}")
    public String refreshConfig(@PathVariable String key) {
        return "Config refreshed, local cache cleared";
    }
}

Manual cache beans (LoadingCache for Guava, direct Cache for Caffeine):

@Bean
public LoadingCache<String, String> guavaManualCache() {
    return CacheBuilder.newBuilder()
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .maximumSize(5000)
            .build(key -> "DB query result: " + key);
}

@Bean
public com.github.benmanes.caffeine.cache.Cache<String, Object> caffeineManualCache() {
    return Caffeine.newBuilder()
            .expireAfterAccess(8, TimeUnit.MINUTES)
            .maximumSize(1000)
            .build();
}

Expiration policies : expireAfterWrite – timer starts when data is written; suitable for data that must be refreshed on a fixed schedule (e.g., configuration, periodic updates). expireAfterAccess – timer resets on each access; keeps hot data resident and automatically evicts cold entries, ideal for high‑frequency hotspot data.

Multi‑cache coexistence in SpringBoot is achieved by defining multiple CacheManager beans and selecting the desired implementation via the cacheManager attribute on @Cacheable or @CacheEvict. This allows static base data to use Caffeine, legacy data to use Guava, and distributed shared data to use Redis without conflict.

Two‑level cache flow :

Query: Caffeine → hit returns; miss → Redis → hit writes back to Caffeine; miss → DB.

Update: DB update → evict Redis entry → evict Caffeine entry.

This pattern dramatically reduces Redis pressure, can double request QPS, and sharply lower response latency.

Conclusion : In high‑concurrency systems, local caches are the ultimate performance lever. Caffeine’s W‑TinyLFU algorithm delivers the best hit rate, throughput and memory efficiency, and, combined with Spring Cache annotations, provides zero‑intrusion, unified policy control and minimal maintenance cost. The mature architecture is a local‑cache + distributed‑cache two‑level combination that delivers high performance, high availability and high stability for enterprise‑grade applications.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

RedisCaffeineGuavaLocal CacheSpringBootCache EvictionTwo-level Cache
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.