Skip to content

0006. Make the cache fail open, always

Status: Accepted Date: 2026-09-10

Context

M4 adds Redis as an optional cache in front of the order repository. Redis is a shared instance, not one-per-pod, so its failure mode is different from PostgreSQL's: PostgreSQL going down means the service has genuinely lost its data, but Redis going down means the service has lost a shortcut to data it can still reach another way.

Decision

We make every Redis failure fail open. CachedOrderRepository catches Exception broadly around every read, write and invalidation call to Redis — connection failures, timeouts, and a payload that fails to parse as a valid Order are all logged at warning and swallowed, and the wrapped repository answers instead. There is no setting that changes this: CacheSettings carries a DSN, a TTL, a connection pool size and a connect and an operation timeout — tuning knobs for how the service talks to Redis — and nothing that selects a different failure behaviour.

Alternatives considered

  • Fail closed. Rejected: it converts a latency degradation the service is deliberately built to survive — Redis is down, PostgreSQL answers a little slower — into a total outage, for a dependency whose entire job was to make things faster, never to make them possible.
  • A configurable mode. Rejected: the failing mode would be the one selected by whoever had not thought carefully about the difference, under exactly the conditions — a production incident, a rushed config change — where the wrong setting does the most damage. A flag whose wrong value takes production down is not a feature worth shipping.

Consequences

A Redis outage costs latency, not availability: every read falls through to PostgreSQL, every response is still correct, and /readyz reports the cache under dependencies without letting its failure affect the status code or the pod's place in load balancing (see 0011).

The cost is that a persistently broken cache is quiet. The service keeps serving normally while every read silently misses, with nothing failing loudly enough to page anyone — a warning-level log line per failed call is the only signal, and at any real request volume that becomes background noise rather than an alert. The cache hit rate has to be a dashboard panel and an alert of its own, because a health check built to survive a broken cache is, by construction, not going to be the thing that notices one.

The specification itself, in the section cited below, describes /readyz as checking "database, cache and storage" and removing the pod from load balancing "when they fail" — read literally, that gates on Redis too. This record and 0011 deliberately narrow gating to the database alone: Redis is one shared instance behind every pod, not one-per-pod the way the database connection is, so gating on it would make every pod unready in the same second the cache degraded, turning the latency-only cost above into a total outage. The specification has no dedicated section on cache resilience or readiness tiering, so section 12 is still the closest citation despite that wording gap.

Full reasoning: spec section 12.