Cache-Aside Pattern with Redis: Performance Without Losing Data Ownership
Why I add Redis
I add a cache after identifying a measurable read hotspot.
I do not start with:
every service should use Redis.
Cache-aside flow:
GET cache key
| hit -> return
|
miss
v
source of truth
|
v
SET cache + TTL
|
returnNestJS example
async getProduct(
productId: string,
) {
const key =
`product:${productId}`;
const cached =
await this.redis.get(key);
if (cached) {
return JSON.parse(cached);
}
const product =
await this.productRepo
.findById(productId);
if (!product) {
throw new NotFoundException();
}
const ttlSeconds =
300 + Math.floor(
Math.random() * 30,
);
await this.redis.set(
key,
JSON.stringify(product),
'EX',
ttlSeconds,
);
return product;
}TTL jitter prevents many keys created at the same time from expiring simultaneously.
Invalidation
After update:
await db.transaction(
async (tx) => {
await tx.product.update(
productId,
changes,
);
},
);
await redis.del(
`product:${productId}`,
);There is a small failure window between DB commit and cache delete.
For high-consistency use cases, I use event-driven invalidation/versioning or avoid caching the field.
Cache stampede
A popular key expires.
10,000 requests miss.
All hit database.
Protection:
const lock =
await redis.set(
`lock:${key}`,
requestId,
'NX',
'EX',
10,
);
if (lock) {
try {
return await refresh(key);
} finally {
await releaseLockSafely(
key,
requestId,
);
}
}
await sleep(50);
return readAgain(key);In production I use a safe compare-and-delete lock release or a library rather than deleting another request's lock accidentally.
Redis outage
My policy depends on source capacity.
For reference-data cache:
Redis down -> read DBFor a cache protecting an already maxed DB:
Redis down -> degrade/rate limitA cache is not free availability.
What I do not cache
- authorization decisions without careful invalidation;
- rapidly changing transactional state;
- huge payloads with low reuse;
- data already fast enough in DB;
- errors for long TTL by accident.
Production checklist
- Define source of truth.
- Measure current bottleneck.
- Define acceptable staleness.
- Add TTL jitter.
- Protect hot misses.
- Monitor hit ratio.
- Monitor Redis latency.
- Define Redis-outage behavior.
- Size memory/eviction policy.
- Test invalidation.
FAQ
Is cache-aside eventually consistent?
Yes, unless invalidation/version techniques provide stronger semantics.
What hit ratio is good?
It depends on workload. A high hit ratio is not useful if the cached data is cheap or stale incorrectly.
Should Redis be shared by many services?
It can be, but keys, memory quotas and failure isolation need clear ownership.
Related architecture guides
- [Microservices Design Patterns I Have Used in Real Enterprise Projects](/articles/microservices-design-patterns-real-enterprise-projects)
- [Database per Service](/articles/database-per-service-data-ownership-microservices)
-
Architect's final take
The cache should remain an optimization. When business correctness starts depending on stale cache state, the architecture has crossed an important boundary.
Your feedback helps prioritize deeper technical content.




