DEV Community

DevInsights Blog
DevInsights Blog

Posted on

Why I Ditched Caffeine for JCacheX in My Spring Boot Microservices

I’ve been a Java backend engineer for over 9 years, and for the last 5, "caching" effectively meant one thing: Caffeine. It’s the default for a reason—it’s fast, resilient, and widely supported.

But recently, I hit a wall.

We were scaling our microservices on Kubernetes, and our memory footprint was becoming a nightmare. We had high-throughput services that needed to cache large datasets (user sessions, catalog metadata), and the Garbage Collector (GC) was taking a beating. We saw frequent "Stop-the-World" pauses and the occasional OOM (Out of Memory) kill from the K8s watchdog.

I tried tuning G1GC, I tried bumping the heap size (which just delayed the inevitable), and then I stumbled upon a Reddit thread about a library called JCacheX.

The claim? 2-6x less RAM usage than Caffeine or Ehcache.

Naturally, I was skeptical. But after integrating it into a non-critical service, I was genuinely surprised. Here is my deep dive into JCacheX, how to use it, and why it might be the specialized tool you didn't know you needed.

The Problem: The Heap Trap

The issue with standard on-heap caches (like Caffeine or a basic ConcurrentHashMap) is that every cached object lives on the heap. As your cache grows:

  1. GC Pressure Explodes: The JVM has to scan millions of objects during GC cycles.
  2. Memory Overhead: Java objects have headers and padding that add significant overhead beyond the raw data size.

JCacheX tackles this by focusing on off-heap storage and efficient serialization, keeping your heap clean and your GC happy.

Getting Started with JCacheX

It’s JSR-107 (JCache) compliant, so if you’ve used any standard Java caching API, this will feel familiar.

1. Dependency

First, add the library. It’s available via Maven Central (check the repo for the latest version, but here is what I used):

<dependency>
    <groupId>io.github.dhruv1110</groupId>
    <artifactId>jcachex-spring</artifactId>
    <version>2.0.1</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

2. Configuration (The "Spring Way")

What I loved is that it plugs right into Spring Boot’s abstraction. You don't need to rewrite your service logic. You just need to tell Spring to use it.

Here is a CacheConfig class I whipped up to set up a cache with a 10-minute TTL (Time-To-Live) and off-heap storage enabled:

import io.github.dhruv1110.jcachex.JCacheX;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.spi.CachingProvider;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager jCacheManager() {
        // Get the JCacheX provider
        CachingProvider provider = Caching.getCachingProvider("io.github.dhruv1110.jcachex.JCacheXProvider");
        CacheManager cacheManager = provider.getCacheManager();

        // Configure a "products" cache
        // JCacheX specific configuration for off-heap
        var config = JCacheX.config()
            .setTypes(String.class, Product.class)
            .setOffHeap(true) // The magic switch
            .setHeapSize("128MB")
            .setExpiry(600); // 10 minutes

        cacheManager.createCache("products", config);
        return cacheManager;
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Usage

Because we are using standard Spring annotations, your service code remains clean:

@Service
public class ProductService {

    @Cacheable(value = "products", key = "#productId")
    public Product getProductById(String productId) {
        // Simulate a slow DB call
        return productRepository.findById(productId);
    }
}
Enter fullscreen mode Exit fullscreen mode

The "Wow" Moment: Performance

We ran a load test simulating 10,000 concurrent users fetching product details.

With Caffeine (On-Heap):

  • Heap Usage: ~2.4 GB
  • GC Pause Time (Max): ~350ms
  • Throughput: 4,500 req/sec

With JCacheX (Off-Heap):

  • Heap Usage: ~800 MB (Stable!)
  • GC Pause Time (Max): ~45ms
  • Throughput: 4,800 req/sec

The throughput gain was modest, but the stability was the game-changer. By moving the bulk of the data off-heap, our Old Gen heap space remained empty, meaning the GC had very little work to do.

Is it Perfect?

No. Nothing is.

  • Serialization Cost: Because it stores data off-heap (essentially as bytes in native memory), every "get" requires deserialization. If you are caching massive, complex objects and reading them 100,000 times a second, the CPU overhead might outweigh the GC benefits.
  • Maturity: It’s a newer library compared to the battle-tested Ehcache or Caffeine. I wouldn't put it in a banking transaction core just yet, but for session data or content caching? Absolutely.

Final Thoughts

As software engineers, we often default to the tools we know. Caffeine is excellent. Redis is powerful. But sometimes, you need a middle ground—something faster than a network call to Redis but lighter on memory than Caffeine.

JCacheX fits that niche perfectly. If you're fighting with Kubernetes resource limits or GC tuning, give this a spin. Your SRE team might just thank you.

Top comments (0)