14 Real-World Scenarios Highlighting Core Distributed Caching Issues in Spring Boot

This article presents fourteen practical scenarios covering the design, pitfalls, strategies, and implementation details of distributed caching in Spring Boot, including cache breakdown prevention, write‑through vs. write‑behind, eviction policies, negative caching, secondary caches, cache‑aside pattern, warming, monitoring, consistency, and custom key generation, all illustrated with concrete code examples.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
14 Real-World Scenarios Highlighting Core Distributed Caching Issues in Spring Boot

Distributed caching is essential for high‑performance systems such as public travel platforms or large e‑commerce sites. The article lists fourteen real‑world scenarios and provides detailed solutions tailored to the Java and Spring Boot ecosystem.

2.1 Designing a Distributed Cache for High‑Traffic Systems

A high‑traffic system should use a distributed cache like Redis or Memcached and apply consistent hashing to shard data across cache nodes, ensuring scalability and fault tolerance.

private final ProductRepository productRepository;
@Cacheable(value = "product", key = "#productId")
public Product getProductById(Long productId) {
  return productRepository.findById(productId).orElse(null);
}

Cache product details to reduce database load.

Use Redis for horizontal scaling.

2.2 Cache Breakdown and Prevention

Cache breakdown occurs when multiple threads simultaneously try to rebuild a missing cache entry, overwhelming the backend. Prevention strategies include:

Distributed lock to ensure only one thread rebuilds the cache.

Never‑expire: proactively update the cache when data changes.

Logical expiration: wrap cached data in a VO with an explicit expiry field; the VO itself does not expire in Redis.

private final Object lock = new Object();
synchronized (lock) {
  if (cache.get(key) == null) {
    cache.put(key, fromDBLoadData());
  }
}
RLock lock = redissonClient.getLock("cacheLock:" + key);
try {
  if (lock.tryLock(10, TimeUnit.SECONDS)) {
    Product product = cache.get(key);
    if (product != null) {
      return product;
    }
    cache.put(key, fromDBLoadData());
  } else {
    // fallback to stale data
    return cache.get(key);
  }
} finally {
  lock.unlock();
}

2.3 Write‑Through vs. Write‑Behind

Write‑Through : data is synchronously written to both cache and database, guaranteeing strong consistency at the cost of higher latency.

Write‑Behind : data is first written to cache and flushed to the database asynchronously, favouring performance over immediate consistency.

Write‑Through for strong consistency (e.g., financial transactions).

Write‑Behind for performance‑critical workloads (e.g., logging systems).

2.4 Cache Eviction Strategies

When memory is limited, eviction policies keep the cache efficient. Common policies:

LRU (Least Recently Used): removes the least recently accessed items.

LFU (Least Frequently Used): removes items with the lowest access frequency.

TTL (Time‑To‑Live): automatically removes items after a configured time.

SET product:101 "{...}" EX 3600  # expires after 1 hour

2.5 Negative Caching

Negative caching stores empty or non‑existent results to avoid repeated database queries for missing keys.

public Product queryProduct(String key) {
  // first check cache
  Product product = cache.get(key);
  if (product != null) {
    return product;
  }
  RLock lock = redissonClient.getLock("cacheLock:" + key);
  try {
    if (lock.tryLock(10, 10, TimeUnit.SECONDS)) {
      product = cache.get(key);
      if (product != null) {
        return product;
      }
      product = fromDBLoadData();
      if (product != null) {
        cache.put(key, product);
      } else {
        cache.put(key, "", 1, TimeUnit.MINUTES); // cache empty marker
      }
      return product;
    } else {
      return cache.get(key);
    }
  } finally {
    lock.unlock();
  }
}

2.6 Enabling Cache in Spring Boot

@EnableCaching
@SpringBootApplication
public class App {}

Key annotations:

@Cacheable : caches method return value.

@CacheEvict : removes data from cache.

@CachePut : updates cache without skipping method execution.

2.7 Secondary Cache (L1 + L2)

L1 is an in‑memory cache (Caffeine, Guava, Ehcache) for fast access; L2 is a distributed cache (Redis, Memcached) for larger capacity.

spring:
  cache:
    cache-names: product
    caffeine:
      spec: maximumSize=500,expireAfterWrite=10m

Workflow:

Attempt to read from L1.

If miss, read from L2.

If still miss, load from DB, then populate L2 and L1.

2.8 Cache Penetration and Prevention

Cache penetration occurs when queries for non‑existent data repeatedly hit the database. Prevention includes caching null values and using Bloom filters.

Bloom filter illustration
Bloom filter illustration

2.9 Cache‑Aside Pattern

The caller updates the database and synchronously updates the cache. On read, the cache is checked first; if absent, the DB is queried and the result is stored in the cache.

private final StringRedisTemplate redis;
private final ProductRepository productRepository;

// Update operation
public void update(Product product) {
  productRepository.update(product);
  redis.opsForValue().set("xxx", new ObjectMapper().writeValueAsString(product));
}

// Query operation
public Product query(Long productId) {
  Product product = redis.opsForValue().get(productId);
  if (product != null) {
    return product;
  }
  product = productRepository.findById(productId);
  if (product == null) {
    redis.opsForValue().set("xxx", "", 1, TimeUnit.MINUTES);
  }
  return product;
}

2.10 Cache Invalidation Strategies

TTL: automatic removal after a set time.

Manual invalidation: explicit removal.

Event‑driven invalidation: use messaging (Kafka, Redis Pub/Sub) to broadcast invalidation events.

// Simple cache operation class
@Service("invCacheService")
public class CacheService {
  public static final String CHANNEL_NAME = "product_cache_invalidation_channel";
  private final Map<String, Product> cache = new ConcurrentHashMap<>();
  public void put(String key, Product product) { cache.put(key, product); }
  public Product get(String key) { return cache.get(key); }
  public void invalidate(String key) { System.out.println("Invalidating cache for key: " + key); cache.remove(key); }
}

// Message listener
@Component
public class RedisPubSubListener {
  public RedisPubSubListener(@Qualifier("invCacheService") CacheService cacheService, RedissonClient redissonClient) {
    RTopic topic = redissonClient.getTopic(CacheService.CHANNEL_NAME);
    topic.addListener(String.class, (channel, msg) -> {
      System.out.println("Received message: " + msg + " from channel: " + channel);
      cacheService.invalidate(msg);
    });
  }
}

// Publishing invalidation
@Service
public class ProductService {
  private final RedissonClient redissonClient;
  private final ProductRepository productRepository;
  public void updateProduct(Product product) {
    productRepository.update(product);
    RTopic topic = redissonClient.getTopic(CacheService.CHANNEL_NAME);
    topic.publish("product:" + product.id());
  }
}

2.11 Cache Warming

Load frequently accessed data into the cache before traffic spikes.

@EventListener(ApplicationReadyEvent.class)
public void warmUpCache() {
  productService.getPopularProducts().forEach(product -> cache.put(product.getId(), product));
}

2.12 Monitoring Cache Performance

Key metrics include hit ratio, miss ratio, eviction counts, latency, memory usage, throughput, and error rate. Tools such as Prometheus, Grafana, or New Relic can collect these metrics, and many cache solutions (Redis, Memcached) provide built‑in monitoring.

2.13 Ensuring Cache Consistency

Cache invalidation on write: delete or mark cache entry stale immediately after DB update.

Write‑back cache: write to cache first, flush to DB asynchronously (higher risk of inconsistency).

Read‑through / Write‑through: cache handles loading and persisting data.

TTL: set expiration time for cache entries.

Versioning: attach version numbers to detect stale data.

Distributed lock: serialize updates to avoid race conditions.

2.14 Custom Cache Key Strategy

When cache keys need complex logic, define a custom KeyGenerator bean and reference it via the keyGenerator attribute.

@Bean
public KeyGenerator customKey() {
  return (target, method, params) -> method.getName() + Arrays.toString(params);
}

@Cacheable(keyGenerator = "customKey")
public Product queryProduct(Long id) { }

The article concludes with a reminder that the content above constitutes the entire technical guide.

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.

javaredisspring-bootcache-consistencydistributed-cachingcache-eviction
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

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.