Caching vs. freshness
The tradeoff between serving data fast from a cache vs. serving data that reflects the latest state, covering TTL strategies, cache invalidation patterns, and how to decide which data can be cached and for how long.
TL;DR
| Data type | Cache aggressively | Serve fresh | Why |
|---|---|---|---|
| Account balance | Never cache during transactions | Always from primary DB | Showing stale balance during a transfer causes support tickets |
| Product catalog | Cache 1-5 min TTL, invalidate on edit | Not needed; changes are infrequent | 2M products, 100K views/min, 1000:1 read-to-write ratio |
| Social media like count | Cache 5-30s TTL | Not needed; approximate is fine | Nobody notices if a count is 5 seconds behind |
| News feed content | Cache 1-5 min with event-driven purge | After a user posts, show their own post immediately | Combination of caching + read-your-own-writes |
| Static assets (JS, CSS) | Immutable cache with content hashing | Never stale; hash changes on deploy | Cache-Control: immutable, max-age=31536000 |
Default instinct: cache everything, then carve out exceptions. Most data tolerates seconds to minutes of staleness. The exceptions (financial balances, inventory during checkout, real-time bidding) are the minority. Start by caching aggressively with TTL-based expiration, then add event-driven invalidation only for the data that truly needs it.
The Framing
Your e-commerce platform caches product data in Redis with a 1-hour TTL. A customer loads the product page and sees a price of $29.99. Twenty minutes ago, the merchandising team changed the price to $24.99. The customer adds the item to their cart, and the cart service reads the fresh price from the database: $24.99. The customer sees a different price on the product page and the cart. They call support.
This is the staleness problem. The product page served cached data that no longer matches the source of truth. The cart service (which reads the database directly) showed the real price. The user's experience fractured because two services had different views of the same data.
The naive fix: "stop caching product data." But without the Redis cache, every product page view hits the database. At 100K page views per minute, the database falls over within minutes. The cache was not optional; it was structural.
The real question is not "should I cache this data?" It is "how stale can this data be before a user is harmed, and what invalidation strategy matches that tolerance?"
I have seen teams swing between two extremes: caching everything with long TTLs (fast but stale) or caching nothing because invalidation is "too complex" (correct but slow). The answer is almost always in the middle, with different staleness tolerances per data type.
How Each Works
Caching: trade freshness for speed
Caching stores a computed result so subsequent requests skip the expensive computation. The most common pattern is cache-aside (lazy population): check the cache first, fall through to the database on a miss, and populate the cache for the next reader.
def get_product(product_id: str) -> Product:
cache_key = f"product:{product_id}"
# Check cache first
cached = redis.get(cache_key)
if cached:
return deserialize(cached)
# Cache miss: fetch from DB
product = db.find_product(product_id)
redis.setex(cache_key, ttl_seconds=300, value=serialize(product)) # 5min TTL
return product
The speed benefit is dramatic. Redis responds in under 1ms. A PostgreSQL query with joins takes 5-50ms. At a 95% cache hit rate, 95 out of 100 requests are served 10-50x faster. The database handles only the 5% that miss the cache, reducing its load by 20x.
The cost: every cached value is a snapshot frozen in time. Until the TTL expires or the entry is explicitly invalidated, readers see the snapshot, not the current truth.
Freshness: always serve the source of truth
The "always-fresh" approach skips the cache entirely and reads from the primary database on every request. This guarantees that every response reflects the latest state.
def get_product_fresh(product_id: str) -> Product:
# Always read from the primary database
return db.find_product(product_id)
This is correct by definition but expensive. At scale, every read hits the database. Connection pools saturate. Query latency increases as load grows. You cannot horizontally scale a single-primary database beyond its hardware limits without adding read replicas (which introduce their own staleness via replication lag).
For your interview: never present "no caching" as a serious option for read-heavy data. The correct framing is choosing the right caching strategy and invalidation pattern, not choosing between caching and no caching.
Four cache interaction patterns
The differences between cache patterns determine who populates the cache and when.
| Pattern | Who populates cache | Write path | Staleness risk | Best for |
|---|---|---|---|---|
| Cache-aside | Application on read miss | App writes DB, invalidates/ignores cache | TTL window (seconds-hours) | Most use cases; simplest to implement |
| Read-through | Cache library on miss | Same as cache-aside | Same as cache-aside | Cache libraries with built-in loaders |
| Write-through | Cache on write | App writes to cache, cache writes to DB | Near-zero (cache always current) | Read-after-write consistency is critical |
| Write-behind | Cache on write, async DB flush | App writes cache only, cache flushes later | Data loss risk if cache crashes before flush | High write throughput, acceptable data loss |
Cache-aside is the default choice for 90% of use cases. Write-through when you need read-after-write consistency. Write-behind only when you can tolerate potential data loss.
Head-to-Head Comparison
| Dimension | Aggressive caching | Always-fresh reads | Verdict |
|---|---|---|---|
| Read latency | Sub-ms from Redis | 5-50ms from database | Caching |
| Database load | Reduced 10-20x at 95% hit rate | Full load on every read | Caching |
| Data freshness | Stale by TTL window (seconds-hours) | Always current | Freshness |
| Consistency across services | Services may see different versions | All services see same state | Freshness |
| Horizontal read scaling | Add Redis nodes cheaply | Requires read replicas (still has lag) | Caching |
| Operational complexity | Cache infrastructure + invalidation logic | Simpler, but DB scaling is harder | Depends |
| Failure mode | Cache crash = temporary DB overload | DB crash = total outage | Caching (graceful degradation) |
| Cost at scale | Redis cluster ($200-800/mo) + DB | Larger DB instances ($2K-10K/mo) | Caching |
| Debugging | "Is this stale?" is a common question | Data always current, easier to reason about | Freshness |
The fundamental tension: response speed vs. data accuracy. Caching serves data faster but risks showing outdated information. Fresh reads are always correct but hit performance limits much sooner.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.