When Should You Use Cache-Aside Pattern in Microservices? Best Practices and Pitfalls
Problem
I was designing a caching strategy for our microservices architecture and got stuck on one question: When is cache-aside pattern actually worth using?
I had heard that cache-aside is the “standard” caching pattern, but I wasn’t sure if it was right for our specific workload. Our product catalog had high read volume, but our inventory system changed constantly. Should I use cache-aside for both? Or should I use different patterns?
After some trial and error, I learned that cache-aside isn’t a universal solution. It works great in some scenarios and creates headaches in others. Here’s what I discovered.
What is Cache-Aside Pattern?
Cache-aside (also called lazy loading) means:
- Application checks cache first
- If data not in cache (cache miss), fetch from database
- Store fetched data in cache for future requests
- Next request hits cache directly
async function getProduct(productId) { // Step 1: Check cache const cached = await redis.get(`product:${productId}`);
if (cached) { return JSON.parse(cached); // Cache hit }
// Step 2: Cache miss - fetch from DB const product = await db.queryProduct(productId);
// Step 3: Store in cache await redis.set(`product:${productId}`, JSON.stringify(product), 'EX', 3600);
return product;}The key characteristic: cache is filled on demand, not proactively.
Decision Flowchart
Before diving into details, here’s a quick decision guide:
┌─────────────────────────────────────┐│ Is your workload read-heavy? ││ (many repeated queries) │└─────────────────┬───────────────────┘ │ YES ──────┼────── NO │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Continue check │ │ Avoid cache-aside│ │ │ │ (write-heavy) │ └─────────┬───────┘ └─────────────────┘ │ ▼┌─────────────────────────────────────┐│ Are query parameters unpredictable? ││ (can't predict what users will ask) │└─────────────────┬───────────────────┘ │ YES ──────┼────── NO │ │ ▼ ▼┌─────────────────┐ ┌─────────────────┐│ USE CACHE-ASIDE │ │ Consider cache ││ │ │ prefetching │└─────────────────┘ └─────────────────┘When to Use Cache-Aside
After testing different scenarios, I found cache-aside works best when:
1. High Read Volume with Repeated Queries
Our product catalog API gets 1000+ reads per minute for the same products. Cache-aside reduced database load by 80%.
Before Cache: DB queries: 1000/minAfter Cache-aside: DB queries: 200/min (80% reduction) Cache hits: 800/min2. Unpredictable Query Parameters
Users search products with random filters: category:electronics, brand:sony, price<100. I can’t predict what combinations they’ll use, so pre-loading everything would waste memory.
Cache-aside fills cache on demand - only what users actually request.
3. Storage Cost Constraints
In our cloud environment, Redis memory costs money. Pre-caching all products would require 4GB. With cache-aside, we only cache frequently accessed items - about 500MB.
4. Data Changes Infrequently
Product details (name, description, images) change maybe once per week. Perfect for caching with reasonable TTL.
When to Avoid Cache-Aside
I tried cache-aside on our inventory system and it failed badly. Here’s why:
1. Write-Heavy Workloads
Inventory updates happen constantly. Every purchase, restock, or adjustment changes counts. Caching this data means:
- Cache invalidation complexity
- Stale data risk
- More work than benefit
// WRONG: Caching rapidly changing dataasync function getInventory(productId) { const cached = await redis.get(`inventory:${productId}`); if (cached) return JSON.parse(cached);
const inventory = await db.queryInventory(productId); await redis.set(`inventory:${productId}`, JSON.stringify(inventory), 'EX', 60); return inventory;}
// After 60 seconds, cache shows "100 units available"// But actual inventory is "50 units" - users get wrong info2. Real-Time Data Requirements
Our stock alerts need current counts. A 60-second stale cache would send false alerts or miss critical lows.
3. First-Request Latency Critical
Cache-aside’s first request always hits database (cold cache problem). For our checkout flow, every millisecond counts. Cache prefetching works better here.
Best Practices I Learned
1. Set Appropriate TTL Per Data Type
Different data has different freshness needs:
const TTL_CONFIG = { product_catalog: 3600, // 1 hour - products change infrequently user_session: 1800, // 30 minutes - session data real_time_inventory: 60, // 1 minute - if you must cache inventory static_config: 86400, // 24 hours - rarely changes};
async function setCacheWithTTL(key, data, dataType) { const ttl = TTL_CONFIG[dataType] || 300; // default 5 min await redis.set(key, JSON.stringify(data), 'EX', ttl);}2. Invalidate Cache on Writes
This is the most common mistake I made initially. After updating a product, users still saw old cached data.
async function updateProduct(productId, newData) { // 1. Update database FIRST await db.updateProduct(productId, newData);
// 2. Invalidate cache AFTER await redis.del(`product:${productId}`);
// Alternative: Update cache directly // await redis.set(`product:${productId}`, JSON.stringify(newData), 'EX', 3600);}The sequence matters: update DB first, then invalidate. If you invalidate first and the DB update fails, you lose both cache and correct DB data.
3. Monitor Cache Hit Ratio
I track hit ratio weekly. If it drops below 70%, I adjust TTL or reconsider what I’m caching.
Week 1: Hit ratio 85%, Avg latency 50ms (good)Week 2: Hit ratio 65%, Avg latency 120ms (investigate) - Found: New product category not cached enough - Fix: Increase TTL for popular categoriesWeek 3: Hit ratio 78%, Avg latency 60ms (improved)4. Hash Query Parameters Consistently
Complex queries need consistent cache keys:
// BAD: Different keys for same queryawait redis.get('products:category=electronics');await redis.get('products:electronics'); // Same query, different key!
// GOOD: Consistent hashingfunction buildCacheKey(params) { const sorted = Object.keys(params).sort().map(k => `${k}=${params[k]}`); return `products:${sorted.join('&')}`;}
// Always generates: products:brand=sony&category=electronics&price<100Common Pitfalls
Pitfall 1: Not Invalidating on Writes
Symptom: Users see stale data after updates Solution: Always invalidate after write operations
Pitfall 2: TTL Too Long
Symptom: Data becomes outdated Example: Set 24-hour TTL on pricing, prices update, customers see wrong prices Solution: Match TTL to data change frequency
Pitfall 3: TTL Too Short
Symptom: Cache becomes useless, high miss rate Example: 5-second TTL on product catalog - most requests miss cache Solution: Analyze read patterns, set TTL based on query frequency
Pitfall 4: Over-Caching
Symptom: Redis memory full, rarely-used data cached Solution: Track what’s accessed, only cache hot data
Pitfall 5: Ignoring Cold Cache Problem
Symptom: First requests always slow after deployment Solution: Consider cache warm-up or hybrid with prefetching
Microservices Considerations
In microservices, each service can choose its own caching strategy:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐│ Product │ │ Inventory │ │ Payment ││ Service │ │ Service │ │ Service ││ │ │ │ │ ││ Cache-aside │ │ NO cache │ │ Cache-aside ││ TTL: 1 hour │ │ (real-time) │ │ TTL: 5 min │└──────────────┘ └──────────────┘ └──────────────┘ │ │ │ └───────────────────┼───────────────────┘ │ ┌───────▼───────┐ │ Redis │ │ Cluster │ └───────────────┘API Gateway can add another caching layer for common API responses, reducing calls to downstream services.
Summary
Cache-aside pattern works well when you have:
- Read-heavy workloads with repeated queries
- Unpredictable query parameters
- Storage cost constraints
- Data that changes infrequently
Avoid it when:
- Data changes rapidly (write-heavy)
- Real-time freshness is critical
- First-request latency matters
- Cache invalidation complexity outweighs benefits
Set appropriate TTL per data type, invalidate on writes, and monitor hit ratios. In microservices, let each service choose its strategy based on its specific workload characteristics.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Redis.io Cache-Aside Pattern Tutorial
- 👨💻 Martin Fowler on Caching Patterns
- 👨💻 AWS Caching Strategies Best Practices
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments