A cache with no invalidation strategy serves stale data indefinitely. After a server-side update — price change, policy revision, content correction — users continue seeing the cached version until they clear app data or reinstall. ISO 25010:2011 reliability.recoverability requires a defined freshness boundary. Beyond staleness, unbounded caches accumulate sensitive data (PII, session-specific content) that should have been evicted after use. GDPR Art. 32 expects that personal data is not retained beyond its necessary lifetime — a cache without TTL enforcement is a retention policy violation by omission.
High because indefinitely-cached data can include stale PII or pricing that misleads users and may violate GDPR Art. 32 data minimization obligations.
Wrap every cache entry with a timestamp and a TTL. Evict on read when the entry is expired, and run a periodic cleanup to prevent storage accumulation.
interface CacheEntry<T> { data: T; storedAt: number; ttl: number; }
export class Cache<T> {
private store = new Map<string, CacheEntry<T>>();
set(key: string, data: T, ttl = 5 * 60_000) {
this.store.set(key, { data, storedAt: Date.now(), ttl });
}
get(key: string): T | undefined {
const entry = this.store.get(key);
if (!entry) return undefined;
if (Date.now() - entry.storedAt > entry.ttl) {
this.store.delete(key);
return undefined;
}
return entry.data;
}
invalidate(key: string) { this.store.delete(key); }
}
// Always invalidate after a mutation:
await updateUser(id, data);
userCache.invalidate(`user_${id}`);
Set TTLs no higher than 86,400 seconds (24 hours). For data containing PII, use a tighter TTL (≤3,600 seconds) aligned with your data retention policy.
ID: mobile-offline-storage.sync-conflicts.cache-invalidation
Severity: high
What to look for: Examine cache implementation. Look for TTL (time-to-live), versioning, or event-based invalidation. Check that stale cache is cleared when updates occur.
Pass criteria: Count all cache-related storage operations. Cache has at least 1 documented invalidation strategy: TTL-based expiration (with a defined TTL of no more than 86400 seconds / 24 hours), manual invalidation on updates, or server-driven invalidation. At least 1 cache cleanup or expiration mechanism must exist.
Fail criteria: No cache invalidation found. Cached data could be served indefinitely even after it is updated remotely.
Skip (N/A) when: Never — cache invalidation is essential for data freshness.
Detail on fail: "No cache invalidation strategy found. Cache persists indefinitely, so users may see stale data until they manually clear cache."
Remediation: Implement TTL-based cache invalidation:
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number; // milliseconds
}
class Cache<T> {
private store = new Map<string, CacheEntry<T>>();
async get(key: string, fetch: () => Promise<T>, ttl = 5 * 60 * 1000): Promise<T> {
const entry = this.store.get(key);
if (entry && Date.now() - entry.timestamp < entry.ttl) {
// Cache hit and not expired
return entry.data;
}
// Cache miss or expired, fetch fresh data
const data = await fetch();
this.store.set(key, {
data,
timestamp: Date.now(),
ttl,
});
return data;
}
invalidate(key: string) {
this.store.delete(key);
}
invalidateAll() {
this.store.clear();
}
}
// Usage:
const cache = new Cache<User>();
const user = await cache.get(`user_${id}`, () => fetchUser(id), 10 * 60 * 1000); // 10 min TTL
// After updating user, invalidate cache
await updateUser(id, data);
cache.invalidate(`user_${id}`);