Architecture Decision Record
ADR-009: Resilience Policies & Recovery Objectives
Status
Accepted (2026-06-14)
Context
The framework already supplies the mechanisms for surviving partial failure — a standard Polly resilience handler (timeout / retry / circuit breaker), the outbox for at-least-once delivery (ADR-003), and database-per-service isolation (ADR-006). What was missing was a stated contract:
- No guaranteed coverage. Resilience is applied per registration site (
AddTypedGrpcClient,AddTypedServiceClient) plus a globalConfigureHttpClientDefaultsdefault inMMCA.Common.Aspire. Nothing stopped a new outbound client from silently shipping with no retry/circuit-breaker. - No recovery objectives. Each consumer deploys its own databases, but RTO/RPO and the single-region-vs-failover decision were undocumented — "we'd figure it out" is not a plan, and an untested backup is not a backup (rubric §29).
Decision
- Resilience is a framework invariant, not a per-call choice. Every outbound
HttpClientand gRPC client registered through the framework's extension methods (AddTypedGrpcClient,AddTypedServiceClient) wires the standard resilience handler, matching the global HTTP defaults inMMCA.Common.Aspire. This is enforced by a fitness function (ResilienceHandlerTestsinMMCA.Common.Grpc.Tests) so the policy cannot silently regress. - Consumers must declare recovery objectives. Each consuming app documents, in its own
infra/DISASTER-RECOVERY.md: RTO/RPO per failure scenario, the backup/restore mechanism, and an explicit, signed-off acceptance of single-region risk (or a multi-region failover plan). A restore must be drilled — the DR doc carries a drill-result table that cannot stay empty. - Graceful degradation is the default posture. When a synchronous dependency is unreachable, the resilience pipeline retries/breaks; cross-service consistency that can be deferred flows through the outbox (ADR-003), which buffers and guarantees eventual delivery after recovery.
Reference objectives (MMCA.ADC — a regional, non-24×7 conference app)
| Scenario | RPO | RTO |
|---|---|---|
| Accidental data loss / bad migration (within retention) | ≤ ~10 min (continuous PITR) | ≤ 2 h |
| Single service DB corruption | ≤ ~10 min | ≤ 1 h (PITR restore-as-new, swap) |
| Full region loss | ≤ 1 h (geo-redundant backup) | ≤ 4 h (geo-restore + redeploy) |
ADC deliberately accepts single-region risk: sub-hour multi-region failover is not worth the cost/complexity at its scale. A different consumer (e.g. a 24×7 store) is expected to set tighter objectives and a failover plan in its own DR doc — the framework does not mandate one set of numbers, only that the numbers exist and the restore is drilled.
Rationale
- Invariant over discipline. A fitness function turns "remember to add resilience" into a build gate — the same approach the framework already uses for the layer rules and the MassTransit-v8 pin.
- Objectives belong to the deployer. RTO/RPO depend on the data and the business, which the framework can't know; it can only require that consumers decide and record them.
- Drilled, not assumed. The single most common DR failure is discovering at 2 a.m. that the backups never restored. Forcing a recorded drill closes the §29 gap that documentation alone leaves.
Trade-offs
- The named gate (
ResilienceHandlerTests,MMCA.Common.Grpc.Tests) asserts that the gRPC client path (AddTypedGrpcClient) registers the standard handler, not the runtime behavior of every policy parameter; parameter tuning is still a review concern. Runtime breaker behavior is no longer wholly untested, though: a separate fault-injection test (ResilienceCircuitBreakerFaultInjectionTests, same project) now drives sustained failures and proves the circuit breaker actually trips and short-circuits.AddTypedServiceClientwires the same standard handler but is not yet covered by an equivalent registration test. - Per-consumer DR docs can drift from reality; the drill-result table is the mitigation (a stale table is a visible smell).
- A gRPC client that needs bespoke timeouts must override the standard handler explicitly rather than opt out of resilience entirely — intentional friction.