WhollySoftware
Back to blogBackend

Caching Layers: What to Cache, Invalidate, and Never Cache

Wholly Software TeamMay 2, 20257 min read
Caching Layers: What to Cache, Invalidate, and Never Cache

The easiest caching win we consistently reach for is caching expensive, rarely-changing reads — product catalogs, pricing tiers, permission sets — with a Redis TTL cache in front of the database. For a retail client's catalog API, this took the average response time on the product listing endpoint from around 180ms to under 15ms, and it removed enough database load that we were able to downsize the read replica tier.

We never cache anything involving real-time inventory counts or payment state, even though the temptation is there when those queries are slow. A stale cache on 'items in stock' leads to overselling, and a stale cache on payment status leads to either double charges or shipped orders that were never actually paid for. Those queries get optimized at the database layer instead — better indexes, materialized views refreshed on a tight schedule — rather than cached.

Cache invalidation on writes is where most of the actual engineering effort goes, more than the caching itself. We use a write-through pattern for anything with a small, well-defined set of cache keys tied to an entity — updating a product invalidates exactly that product's cache key. For data with fan-out effects, like a category price change affecting every product in it, we've had better luck with short TTLs (60-120 seconds) plus event-driven invalidation than trying to enumerate every affected key on write.

We got burned once by caching at too coarse a granularity — a single cache key for 'all active promotions' meant that updating one promotion invalidated and forced a rebuild of a query touching every promotion in the system, under load, on every edit. Splitting that into per-promotion keys with a separate small 'active promotion IDs' list fixed the thundering herd problem that coarse key caused during a flash sale.

For read-heavy endpoints where slightly stale data is acceptable, we default to cache-aside with a modest TTL rather than trying to build a fully consistent invalidation system — it's less code, fewer edge cases, and the staleness window (usually under a minute) is well within what the business actually needs. We reserve the more complex event-driven invalidation for the handful of endpoints where staleness has a real cost.

CachingRedisPerformanceBackend
Caching Layers: What to Cache, Invalidate, and Never Cache — Wholly Software