A cache is not a “speedup” but a deliberate weakening of consistency: the TTL is an SLA on staleness, and it must be derived from a business requirement (“the price may be wrong for 60 seconds”), not from “eh, let's say 5 minutes.” Event-driven invalidation with a TTL as a safety floor is a good default precisely as a pair: invalidations get lost not because of delivery semantics but because of operational failures (a crashed relay, an overflow, a routing error, a desync after a rebalance), and the TTL bounds the damage of such a loss. This is a default, not an obligation: durable write-through with explicitly stated guarantees is also correct.
The best-documented account of this at scale is Nishtala et al., “Scaling Memcache at Facebook” (NSDI 2013). Two of its mechanisms answer the failures above: invalidations are driven from the database’s commit log rather than from application code, so a crashed web server cannot skip one, and leases stop a slow reader from writing a stale value back after a newer write deleted the key — the race every cache-aside implementation has by default. HTTP made staleness explicit in the protocol: Cache-Control: max-age and stale-while-revalidate (RFC 5861, on top of the caching semantics of RFC 9111) state how old a response may be and for how long it may be served while refreshing.
// TTL as a contract, not a constant buried deep in the code
type CachePolicy struct {
TTL time.Duration // = the documented staleness budget
StaleWhileRevalidate time.Duration // serve stale, refresh in the background
}
var pricePolicy = CachePolicy{TTL: 60 * time.Second, StaleWhileRevalidate: 10 * time.Second}
// review question: "why 60?" must have an answer citing the requirementCache-aside / read-through / write-through / write-behind is not a matter of taste: write-behind loses data when the cache goes down and is acceptable only for recoverable data (counters, metrics). A key with two writers (the service and the invalidator) is a race by construction; version the value or write through a single point.
Where no staleness budget exists. Some reads must not be stale at all: a balance checked before a withdrawal, a permission just revoked, an idempotency key (22). There the cache either is bypassed for that read or becomes part of the write path with the same guarantees as the database. If nobody can state the budget in business terms, that is the finding: the cache is hiding a consistency decision nobody made. Stampedes on expiry are a separate problem (71).