Part 1 explained caching layers and failure modes. This part is about keeping caches correct when data changes in a distributed system: versioned keys, event-driven invalidation, and the consistency trade-offs you actually live with.
1) Versioned caches: invalidate by changing the key
Instead of deleting keys, you bump a version and let old keys expire naturally.
Key pattern:
user:{id}:v{version}
How it works:
- Store the current version in DB or a small metadata store.
- Reads fetch the version, then read the versioned cache key.
- Writes increment the version.
Pros
- Invalidation is cheap and reliable (no delete storms).
- Old keys can expire with TTL in the background.
- Works across many caches and services.
Cons
- Extra read (to get version) or version is cached too.
- Requires a version store with strong write semantics.
Where it shines
- User profiles, product pages, or any entity with frequent updates.
- Multi-service systems where you cannot fan-out invalidations reliably.
2) Pub/Sub invalidation: push change events
In this model, writes publish an event. Consumers invalidate or update their caches.
Event payload (example):
{
"entity": "user",
"id": "123",
"action": "updated",
"ts": 1734790000
}
Typical flow
- Write to DB
- Publish event
- Cache owners receive event
- Invalidate or update cache
Why it fails if you do it naively
- Events can be dropped, delayed, or reordered.
- Consumers can be down when events publish.
How to make it reliable
- Use an outbox pattern (store events in DB, publish asynchronously).
- Make handlers idempotent.
- Use a durable stream (Kafka/NATS) instead of volatile pubsub when correctness matters.
3) TTL is not enough; use soft TTL and stale-while-revalidate
TTL alone creates stampedes and synchronized expirations.
Better pattern
- Soft TTL: consider cache stale before it truly expires.
- Serve stale data briefly while one request refreshes in background.
- Add jitter to TTL to avoid thundering herds.
Example strategy
- Hard TTL: 10 minutes
- Soft TTL: 8 minutes
- If age > 8 minutes, refresh asynchronously
4) Stampede control: avoid N concurrent recomputations
When popular keys expire, you can melt the DB.
Mitigations
- Singleflight (request coalescing)
- Locking on recompute
- Probabilistic early refresh
- Pre-warming hot keys
5) Consistency is a choice, not a bug
There is no perfect cache consistency, only explicit trade-offs.
Common models
- Eventual consistency: cheap, fast, some staleness
- Bounded staleness: TTL-based, measurable drift
- Read-your-writes: require versioning or session caches
- Strong consistency: usually too expensive to maintain for caches
Rule of thumb
- If a bug is catastrophic, skip caching for that path.
- If staleness is acceptable, make it measurable and visible.
6) A practical decision matrix
Use this to pick a strategy per domain:
- User profile: versioned keys + TTL + background refresh
- Search results: TTL + soft TTL + async refresh
- Feature flags: pub/sub with durable stream + short TTL
- Financial balances: avoid cache or use read-your-writes session cache only
Final takeaway
Correctness in caches is not automatic. It requires:
- deterministic invalidation (versioning),
- durable change propagation (pub/sub + outbox),
- and explicit consistency guarantees.
Part 3 shows how to build a cache-aside layer in Go with these patterns baked in.