Java Application Cache Invalidation for SQL Query Results

Strategies for Invalidating SQL-Result Caches in Java Applications

Cache invalidation for database query results is critical to ensure data consistency while retaining performance benefits. Below is a practical, production-ready approach using modern Java constructs and common caching libraries.

1. High-Level Workflow

The process involves three coordinated components:

Phase Description
Orchestration Schedule cache refresh at configurable intervals using ScheduledThreadPoolExecutor
Trigger Execute a thread-safe cache-clearing oepration targeting specific SQL result caches
Implementation Use cache abstraction (e.g., Spring Cache, Caffeine) with key-based eviction or full reset

2. Implementation Details

Step 1: Configure a Robust Scheduler

Prefer ScheduledThreadPoolExecutor over single-threaded varients for resilience and scalability:

private final ScheduledExecutorService cacheRefresher =
    Executors.newScheduledThreadPool(2, new ThreadFactory() {
        private final AtomicInteger counter = new AtomicInteger(1);
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "cache-refresh-pool-" + counter.getAndIncrement());
        }
    });

// Schedule initial execution after 5 seconds, then every 30 minutes
cacheRefresher.scheduleAtFixedRate(
    this::evictStaleQueryResults,
    5L, 30L, TimeUnit.MINUTES
);

Step 2: Trigger Cache Eviction Safely

Encapsulate eviction logic in a dedicated method that avoids blocking the scheduler thread:

private void evictStaleQueryResults() {
    try {
        // Log before eviction for observability
        log.info("Initiating scheduled eviction of SQL query caches");

        // Delegate to cache manager — avoid hardcoding eviction logic here
        cacheManager.invalidateAllQueryCaches();

        log.info("SQL query caches successfully refreshed");
    } catch (Exception e) {
        log.error("Failed to refresh SQL caches", e);
        // Optional: notify monitoring system (e.g., via Micrometer counter)
    }
}

Step 3: Implement Cache Management Layer

Use Caffeine with explicit cache names for SQL result sets. Example with manual cache control:

@Component
public class SqlResultCacheManager {

    private final LoadingCache<String, List<Map<String, Object>>> userSummaryCache;
    private final LoadingCache<String, Map<String, Object>> configCache;

    public SqlResultCacheManager() {
        this.userSummaryCache = Caffeine.newBuilder()
            .maximumSize(500)
            .expireAfterWrite(1, TimeUnit.HOURS)
            .build(query -> executeQueryAndCache(query));

        this.configCache = Caffeine.newBuilder()
            .maximumSize(100)
            .refreshAfterWrite(10, TimeUnit.MINUTES)
            .build(key -> fetchConfigByKey(key));
    }

    public void invalidateAllQueryCaches() {
        userSummaryCache.invalidateAll();
        configCache.invalidateAll();
    }

    private List<Map<String, Object>> executeQueryAndCache(String sql) {
        // Actual JDBC or JPA-based query execution
        return jdbcTemplate.queryForList(sql);
    }

    private Map<String, Object> fetchConfigByKey(String key) {
        return jdbcTemplate.queryForObject(
            "SELECT * FROM app_config WHERE key = ?", 
            new BeanPropertyRowMapper<>(HashMap.class), key);
    }
}

3. Alternative: Declarative Caching with Spring

If using Spring Boot, leverage @Cacheable, @CacheEvict, and @EnableCaching:

@Service
public class ReportService {

    @Cacheable(value = "salesReports", key = "#year + '-' + #region")
    public List<SalesRecord> fetchAnnualSales(int year, String region) {
        return salesRepository.findByYearAndRegion(year, region);
    }
}

// Externalized cache invalidation
@Component
public class CacheInvalidationScheduler {

    @Autowired
    private CacheManager cacheManager;

    @Scheduled(fixedDelay = 1800000) // 30 min
    public void clearReportCache() {
        cacheManager.getCache("salesReports").clear();
    }
}

4. Key Considerations

  • Granularity: Prefer targeted eviction (e.g., by cache name or key pattern) over global clears.
  • Concurrency: Ensure cache operations are thread-safe—Caffeine and Spring Cache handle this internally.
  • Observability: Add metrics (e.g., eviction count, latency) using Micrometer or Dropwizard Metrics.
  • Fallback: Design for graceful degradation if cache refresh fails (e.g., serve stale-on-error).

Tags: java Caffeine spring-cache scheduled-executor JDBC

Posted on Tue, 20 Oct 2026 16:20:10 +0000 by ianhull