
When to use cache and when not to
- Backend , Golang , System design
- August 22, 2025
Table of Contents
There is a production bug that I still remember vividly: user A cancels an order, but the app still displays “delivering” for the next 10 minutes. Support receives 30 tickets in one morning. The reason: cache TTL is 10 minutes, but no one invalidates it when the order status changes.
That was the first time I understood: cache is not always your friend. Used incorrectly, it is the number one enemy of data consistency.
This article does not discuss LRU, LFU, or eviction policy. I will focus on the most important question that few people take the time to think about: when to cache, when not to, and how to avoid regretting it a month later.

When to Use Cache
1. Read-Heavy, Write-Light - “Pre-Cooked” Data
User profiles, product catalogs, system configurations - rarely changed but frequently accessed. This type of data is ideal for caching.
// Pattern: cache-aside
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
// Check cache first
if data, err := s.cache.Get(ctx, "user:"+id); err == nil {
return decodeUser(data), nil
}
// Cache miss → query DB
user, err := s.db.GetUser(ctx, id)
if err != nil {
return nil, err
}
// Set cache for next time
s.cache.Set(ctx, "user:"+id, encodeUser(user), 10*time.Minute)
return user, nil
}
Cache-aside is the simplest pattern and the one I use the most. Advantages: easy-to-understand code, no magic. Disadvantages: the first cache miss is still slow.
2. Hot Spots - 20% of Data Receives 80% of Requests
Flash sales, viral articles, prominent merchants. When 80% of traffic only hits 20% of the data, caching immediately reduces the load on the DB significantly. This is the largest quick win in terms of ROI in performance optimization.
// Pattern: cache top-N hot items
func (s *Service) GetHotMerchants(ctx context.Context) ([]Merchant, error) {
if data, err := s.cache.Get(ctx, "merchants:hot"); err == nil {
return decodeMerchants(data), nil
}
merchants, err := s.db.TopByOrders(ctx, 50) // top 50 merchants
if err != nil {
return nil, err
}
s.cache.Set(ctx, "merchants:hot", encodeMerchants(merchants), 5*time.Minute)
return merchants, nil
}
3. Accepting Stale Data for a Few Seconds
View counts, like counts, rankings - a 2-3 second discrepancy is not noticeable to users. But if COUNT(*) is executed every time the page is refreshed, the DB will be overwhelmed.
4. Expensive Queries - Aggregation, Reports, Dashboards
Queries that take 5-10 seconds to run should have their results cached and refreshed periodically:
func (s *Service) GetDashboard(ctx context.Context) (*Dashboard, error) {
key := "dashboard:daily"
if data, err := s.cache.Get(ctx, key); err == nil {
return decodeDashboard(data), nil
}
dash, err := s.computeDashboard(ctx) // expensive query taking 8 seconds
if err != nil {
return nil, err
}
s.cache.Set(ctx, key, encodeDashboard(dash), 15*time.Minute)
return dash, nil
}
When NOT to Cache
1. Highly Volatile Data - “Cache is a Disaster”
Account balances, real-time inventory, order status. Caching here → users see incorrect balances, buy out-of-stock items. Read directly from the DB or use event-driven updates.
2. High Cardinality - Key Space Explosion
// ❌ Wrong: cache key too detailed → millions of keys, hit rate near 0
cacheKey := fmt.Sprintf("products:c=%s&p_min=%d&p_max=%d&sort=%s&page=%d",
category, minPrice, maxPrice, sortBy, page)
// ✅ Correct: cache at a higher level, filter/paginate at the app layer
cacheKey := "products:popular" // cache top 200, filter in memory
The more keys there are, the lower the hit rate, and caching becomes an overhead instead of an optimization.
3. Upstream is Already Fast - Don’t Optimize What’s Not Needed
DB queries return in 2ms with the correct index. Adding cache only creates complexity and adds a potential point of failure. I’ve seen teams cache responses from already fast endpoints - latency increases from 2ms to 3ms due to the Redis network hop.
4. No Invalidation Strategy
If you don’t know when and how to invalidate the cache when data changes - don’t cache. Displaying old data to users is worse than not caching at all.
Invalidation Strategy
| Strategy | Use Case | Warning |
|---|---|---|
| TTL | Data rarely changes, accepting stale data for a few minutes | Stale data within the TTL window |
| Write-through | Need to sync immediately, few writes | Increased write latency |
| Event-driven | Data consistency is the most important | Most complex to implement correctly |
The write-through pattern code I used:
func (s *Service) UpdateProduct(ctx context.Context, p *Product) error {
if err := s.db.Update(ctx, p); err != nil {
return err
}
// Invalidate related cache keys
s.cache.Delete(ctx, "product:"+p.ID)
s.cache.Delete(ctx, "products:popular")
s.cache.Delete(ctx, "products:category:"+p.CategoryID)
return nil
}
Don’t forget to invalidate all cache keys containing old data. I once deleted product:123 but forgot products:popular → the list page still displayed the old price. This bug took 4 hours to fix.
Decision Rules
| Criteria | ✅ Cache | ❌ No Cache |
|---|---|---|
| Read/Write ratio | Read more than write 10x+ | Write more than read |
| Data freshness | Acceptable stale OK | Need real-time |
| Complexity benefit | Benefit > maintenance cost | DB is fast enough |
| Invalidation plan | Clear strategy | “Leave it for later” |
“Cache is like a thermos - super useful for best-sellers, but if left for too long, the coffee also turns sour.”
Bottom line
Cache is a double-edged sword. Used correctly: p99 latency decreases 10x. Used incorrectly: data inconsistency, the hardest-to-debug bug ever. Before caching anything, ask 3 questions: (1) How often does this data change? (2) Do I have an invalidation plan? (3) What is the actual benefit in milliseconds?
If you can’t answer all 3 - stick to the original, query the DB, and sleep better.
Have you ever encountered a “cache displaying old data” bug? Is there any effective invalidation pattern that I haven’t tried? Share your war story about cache with me - lessons from cache bugs are always the most valuable. 🍵

