Course outline

Spring Boot / Intermediate: data, security, and framework evidence / Lesson 11

Caching keys and consistency

Define what may be reused, for whom, and for how long before selecting Spring caching annotations or a provider.

About 5 min readContent reviewed
On this page

Lesson goals

  • Choose complete cache keys and safe result ownership.
  • Explain provider-specific size/expiry behavior.
  • Coordinate invalidation with transaction outcomes and security.

A cache changes a freshness contract

Caching reuses a result under a key instead of executing the underlying method every time. Define tolerated staleness, scope, invalidation, memory budget, and failure behavior first. A read-only annotation or a high hit rate does not prove a cached result is safe for every user. Avoid caching sensitive authorization decisions or user-specific data under a key that omits identity and policy context.

Boot can configure cache infrastructure when caching is enabled and an appropriate provider is available. The abstraction does not invent a universal TTL, distributed lock, or atomic database/cache transaction. A simple concurrent-map cache has different behavior from Caffeine or a remote provider. Pin the selected provider's contract and test it rather than assuming annotations erase those differences.

Declare the dimensions actually used by the result

The fragment enables caching in a separate configuration and includes tenant, product, and language dimensions through positional parameter references. This avoids pretending that product id alone identifies every representation. It requires a chosen CacheManager and contextual query capability, and it is not executed. The tenant input must still be derived or checked through trusted authorization.

The default proxy-based caching mode also needs a real managed call boundary; a same-instance call can bypass the advice. A cache hit skips underlying method execution, so essential authorization should not exist only inside work that the hit bypasses. Keep the access decision and result reuse contract deliberately aligned, including revocation and changes to user-visible fields.

java Illustrative scope-complete cache key
package training.catalog;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;

@Configuration(proxyBeanMethods = false)
@EnableCaching
class ProductCacheConfiguration {}

@Service
class ProductCacheService {
    private final ProductQuery products;
    ProductCacheService(ProductQuery products) { this.products = products; }
    @Cacheable(cacheNames="productSummaries", key="{#p0,#p1,#p2}")
    public ProductSummary load(String tenantId, long productId, String language) {
        return products.read(tenantId, productId, language);
    }
}

The list key includes all three declared dimensions. Cache provider, interception, authorization, and invalidation behavior remain unexecuted context.

Check key separation without claiming provider behavior

The JDK-only record key model shows that the same product id in another tenant or language is a different key. Its three Boolean results verify that synthetic equality contract. It is not a Spring cache test, an expiry test, or proof that a caller is entitled to the tenant string it supplied. An attacker-selected scope remains unsafe even inside a technically distinct cache key.

Cached values need an ownership rule too. Reusing a mutable list or a managed entity can let one caller affect another's result or retain persistence state unexpectedly. Prefer an approved immutable result or defensive copy where the contract requires it. Negative caching of missing results also needs a freshness policy so later creation is not hidden longer than intended.

java Dependency-free key-dimension model
public class Main {
record Key(String tenant,long product,String language) {}
 public static void main(String[] args) throws Exception {
var cache=java.util.Map.of(new Key("north",42,"en"),"North view");
System.out.println(cache.containsKey(new Key("north",42,"en")));
System.out.println(cache.containsKey(new Key("south",42,"en")));
System.out.println(cache.containsKey(new Key("north",42,"ar")));
 }
}
Expected output
true
false
false

Only the identical tenant/product/language key matches. No cache provider, TTL, proxy, or authorization mechanism is run.

Align invalidation with actual committed change

Eviction after a method invocation is not automatically proof it occurred after a database commit. Cache and transaction advice ordering, failure, and provider behavior matter. A transaction-aware decorator or explicit after-commit workflow can coordinate selected operations, but does not make a distributed cache and database one atomic resource. Test rollback and commit failure as well as ordinary updates.

Define which keys a mutation invalidates, how other instances learn about it, and what happens if invalidation delivery fails. Avoid clearing every cache globally as a routine cure for one stale value. sync options and per-process coordination do not imply one cluster-wide lock. Cache stampedes, expensive misses, and expiry bursts need bounded work and provider-aware measurements.

Measure usefulness and stale-data consequences

Test key collisions, tenant/language separation, mutable results, negative results, update invalidation, rollback, expiry, capacity, and backend failure. A mock CacheManager cannot prove a remote provider's serialization or distributed behavior. Record hit/miss and latency under comparable workloads, but retain business correctness and access denial checks even when the hit ratio looks excellent.

No remote cache service is started here. Keep provider-specific configuration illustrative until validated in an approved isolated environment. A useful cache improves measured work while respecting a stated staleness and security contract. If nobody can explain when a result is refreshed or who may reuse it, adding @Cacheable has hidden a design problem rather than solved a performance one.

Common errors and fixes

A tenant received another tenant's cached DTO

Why it happens
The cache key or reuse authorization omitted scope.
How to fix it
Repair trusted access and complete the key dimensions.
Prevent it next time
Test valid cross-tenant identifiers.

A simple cache never expired entries

Why it happens
Provider defaults were assumed to supply TTL or capacity.
How to fix it
Choose and verify provider-specific limits.
Prevent it next time
Document expiry and memory policy.

Rollback left a misleading cached value

Why it happens
Cache mutation and database completion were not coordinated.
How to fix it
Use a reviewed transaction-aware/after-commit policy and test failures.
Prevent it next time
Include rollback and commit-time rejection cases.

Check your understanding

1. Why does product id42 alone make a poor key for tenant-local translated summaries?

Show worked solution

The same id can identify different tenant data or another language representation. Include every result-defining dimension and enforce trusted scope independently. A complete equality key does not authorize a caller to choose another tenant.

2. Does a successful direct load call prove Cacheable interception?

Show worked solution

No. It can prove underlying method behavior. The caching contract needs a managed proxy call, selected provider, key checks, and hit/miss/invalidation evidence. Self-invocation or manual construction can bypass advice while still returning a correct-looking DTO.

Key takeaways

  • A cache is a scoped freshness and ownership contract.
  • Provider behavior is not supplied universally by annotations.
  • Transaction outcome and authorization remain necessary on cache paths.

Official references