Failure Models
Purpose
Fix the fault model in writing before designing anything else. Every downstream decision —
whether a retry is safe, whether a read may be stale, how many replicas are enough — is an
answer to "which faults do we tolerate?", and a design that never asked the question has
answered it by accident.
The failure this prevents is the one-word fault model. "The service can go down" is not a
model: it silently assumes crash-stop, which lets a developer write recovery code that is
not idempotent, treat a timeout as a definite failure, and count three replicas on one host
as three. Naming the class turns each of those into a visible, arguable claim.
Workflow
- Write a fault-model card for each boundary. Name the fault classes, failure domains,
synchrony assumption, recovery source, detection mechanism, and maximum tolerated
combination. Crash-stop, crash-recovery, omission, timing and Byzantine are not labels
for the whole system: a trusted database replica may be crash-recovery while an
Internet-facing client is arbitrary or hostile. A process that restarts from durable
state is crash-recovery; any in-flight operation whose completion was not durably
recorded must be retried, reconciled, or abandoned by an explicit rule.
- Give every remote call three outcomes, not two: success, definite failure, unknown.
Definite failure means the request provably never applied; anything else — read timeout,
reset after the bytes went out, a broker ack that never arrived — is unknown. Then decide
per call what happens on unknown: retry (safe only if idempotent), reconcile later, or
escalate to a human. "Retry and hope" is a decision too; make it explicit. See
references/the-unknown-outcome.md.
- Add gray failure to the model. Assume a node that is up, passing its health check, and
answering at ten times its normal latency. If the design has no answer for that node, it
has no answer in production either.
- Draw the failure domains. For a process, a host, a rack, an AZ, a dependency and a
deploy, write what each one takes down. Replicas sharing a domain fail together for that
cause; they may still tolerate independent process faults.
- Do conditional availability arithmetic on the request path before promising a
number. Required dependencies in series multiply availability only when their events are
independent and their SLI windows and success definitions align; genuinely independent
redundant alternatives multiply unavailability. Correlated and conditional failure
needs a measured joint distribution or an explicit common-cause model. See
references/failure-domains-and-arithmetic.md.
- Walk the eight fallacies as a checklist — reliable network, zero latency, infinite
bandwidth, secure network, unchanging topology, one administrator, zero transport cost,
homogeneous network. Each is checkable in code, not just prose: a client with no read
timeout has asserted the first, an unbounded in-memory queue the third, a hostname
resolved once at startup the fifth.
Inspect the actual client/driver, retry, durability and deployment configuration before assigning
outcomes. The conceptual Java type uses sealed classes/records (Java 17); exhaustive pattern
switch without preview requires Java 21. Preserve the target. Deliver the boundary's card,
evidence versus assumptions, unresolved outcome policy and one fault-injection case with an
observable invariant. Missing protocol or topology evidence means a conditional claim, not a
replica count or availability promise.
Fault classes
Assume crash-stop when:
- the algorithm may treat a stopped participant as never returning. Replacing its process
identity with a fresh replica does not make the original participant crash-recovery.
Assume crash-recovery when:
- the same logical participant can return after a crash and recover durable state — local
log, database rows, committed offsets, epochs or leases. Volatile state is lost; durable
state may lag acknowledged work unless the durability contract proves otherwise.
Assume omission (a message or a response silently lost) when:
- the underlying transport, queue, proxy or load balancer can drop sends or receives.
A higher-level reliable-channel abstraction may mask omissions, but its retry,
deduplication and terminal-failure assumptions then become part of the model.
Assume timing/performance failure when:
- correctness or usefulness depends on a deadline. A correct-but-late response can be a
failure to the caller. Allocate the caller's end-to-end deadline across attempts,
queueing and cleanup; a timeout is not automatically useful merely because it is shorter.
Include Byzantine faults when:
- input crosses a trust boundary — a client, another tenant, a third party — where a
participant may send arbitrary, inconsistent or hostile data. Input validation protects
an API but does not make its replication protocol Byzantine-fault tolerant. Under common
quorum protocols, tolerating `f` crash failures typically needs `2f+1` voting members and
Byzantine agreement commonly needs `3f+1`; the exact bound depends on synchrony,
authentication, quorum and protocol assumptions. State those assumptions instead of
transplanting a replica count.
Fault-model card
For every important operation, make these fields reviewable:
| Field |
Question that must have an answer |
| Safety invariant |
What must remain true even during a partition, retry or recovery? |
| Liveness condition |
Under which timing and quorum assumptions must progress resume? |
| Faults tolerated |
Crash-stop, crash-recovery, send/receive omission, delay, corruption, arbitrary peer? |
| Bound |
How many simultaneous faults, and in which independent domains? |
| Detector |
Timeout, lease, heartbeat, quorum observation, operator signal? Which false suspicion is acceptable? |
| Durable truth |
Which log, row, offset, epoch or manifest reconstructs state after restart? |
| Ambiguous effect |
How is an unknown outcome deduplicated, queried, reconciled or escalated? |
| Recovery objective |
What RTO/RPO and backlog-drain time are required, and under what load? |
| Re-entry |
How is a recovered or partitioned participant fenced before it can mutate state again? |
Do not merge fault, error and failure. A fault is the hypothesised cause; an error
is incorrect internal state; a failure is externally visible deviation from the service
contract. The distinction prevents a host reboot from being counted as one customer-visible
failure per request and prevents a latency SLO failure from being dismissed because every
response was eventually correct.
Rules
- Partial failure creates outcome uncertainty. A local exception does not imply rollback
either; remote calls additionally decouple caller observation from peer execution. A remote
call can leave you not knowing — and that third outcome is where many
distributed bugs arise. Code that maps a timeout onto "it failed" has erased it: a timeout
states the caller's patience, never the callee's state, and the callee may complete the
work after the caller gave up.
- Crash-recovery makes the recovery path a correctness surface. For each recovery step,
state whether it is idempotent and under which key. The mechanics are
idempotency; the
requirement to have an answer is here.
- A slow node can be more damaging than a dead one. A definitively stopped endpoint is
eventually excluded; a slow endpoint may retain traffic and consume caller threads,
connections and deadline budget. But aggressive suspicion can eject a healthy node and
destroy quorum or capacity. That the system's view of health can differ from the client's
— differential observability — is why "fail fast" is a policy, not a fact. In a fully
asynchronous network a detector cannot distinguish crash from unbounded delay; practical
systems assume some eventual timing bound and trade false suspicion against detection
delay. Record that trade-off for readiness checks, leases and failover.
- Redundancy must name the fault it tolerates. Three replicas on one host cannot survive
losing that host, but may tolerate an individual process crash. Ask what they share: host, rack, AZ, control plane,
image, config, deploy, certificate, downstream dependency. A shared deploy is the one most
often missed — rolling one bad artefact to every replica introduces a common cause, which
makes deploy strategy an availability control rather than a release convenience.
- Adding a required dependency multiplies availability under an independence model and
therefore increases total unavailability. Ten independent dependencies at 99.9% in
series produce about 99.0% path availability. For a time-based SLI on a 365-day year,
that corresponds to roughly 87 unavailable hours, not 8.8; request failure fractions do
not directly convert to outage hours. Real incidents are often correlated, so use this as a comparison model, not a
forecast. Either define a tested degraded mode that removes the dependency from the
required path, or stop quoting the higher number. Redundant alternatives multiply
unavailability only under independence; common causes set an unavailability floor.
Decision framework
If the operation crosses a process boundary:
classify timeout/cancellation/disconnect as Unknown unless protocol evidence proves
the request could not have applied.
If progress requires suspecting a peer:
preserve safety with quorum, epochs or fencing;
tune the detector only for liveness and recovery speed.
If replicas share any host, zone, control plane, deploy, credential or dependency:
model that cause once as a common failure domain;
do not multiply replica availability as if independent.
If a recovered participant can still write:
require a new epoch/term/fencing token or an authoritative ownership check before re-entry.
If the design claims availability during partition:
state which operations remain safe, which side may progress, and what reconciliation
occurs after healing. "The service stays up" is not a consistency contract.
Failure injection and recovery proof
Test the model, not merely exception handlers:
- inject loss separately before send, after apply/before acknowledgement, and during
response transfer; assert downstream state and duplicate count;
- pause a process and add latency/jitter rather than testing only clean termination;
- partition asymmetrically (
A reaches B, B cannot reach A) and isolate data plane
from control plane;
- crash after every durable-write boundary, restart from persisted state, and verify the
safety invariant plus bounded recovery;
- expire credentials, deploy incompatible versions, exhaust pools/disk/file descriptors,
and restore backups into an isolated environment;
- run at realistic load: failover that takes 20 seconds when idle can create hours of
recovery backlog at saturation.
Observe detection latency, false-positive rate, unknown outcomes, duplicate/reconciliation
counts, quorum loss, recovery backlog, RTO and recovered data point (RPO). A test that only
asserts the client exception does not validate the distributed outcome.
Anti-patterns
| Anti-pattern |
Why dangerous / symptom |
Better alternative |
| One fault model for the whole system |
Trust and durability assumptions leak across boundaries |
Model each operation and participant role, then compose them |
| Timeout means rollback |
Retried writes duplicate after lost acknowledgements |
Preserve Unknown; use idempotency, status lookup or reconciliation |
| Health check means truth |
Gray/asymmetric failures stay green or healthy nodes flap |
Compare client-view signals; define detector error costs |
| Replica count means availability |
Common deploy, zone or datastore defeats all replicas |
Draw domains and measure joint/common-cause failures |
| Failover equals recovery |
Traffic moves but stale owners write, data is missing, backlog explodes |
Fence old owners; prove state recovery and capacity during catch-up |
| Chaos without invariants |
Generates outages but no falsifiable learning |
Declare safety/liveness hypotheses, blast radius and abort conditions first |
References
- The unknown outcome — the three-outcome model in Java,
how a JDBC, HTTP and Kafka call each maps onto it, and what an unknown write forces the
design to provide. Read when adding a retry, handling a timeout, or writing across a
process boundary.
- Failure domains and availability arithmetic
— series and parallel composition worked through, correlated failure, and the questions
that expose a hidden shared dependency. Read when promising an availability number, sizing
replicas, or reviewing a topology.
1---2name: failure-models3description: Stating a system's fault model before designing against it: crash-stop, crash-recovery, omission, timing and Byzantine faults; partial failure and the third outcome of every remote call (unknown); gray failure and the slow node whose health check stays green; the eight fallacies as a checklist; blast radius, correlated versus independent failure, and the availability arithmetic of a dependency chain. Use when a design says "if the service is down" without defining down, when a retry is added to a call whose outcome is unknown, when a node is slow rather than dead, when replicas share a host, an AZ or a database, or when an availability target is quoted for a service built on ten others. Does not cover what the model implies about messages (delivery-semantics) or reads (consistency-models), how a failure spreads (cascading-failures), the named shapes (distributed-failure-catalogue), what an orchestrator does with a failed pod (kubernetes-service-lifecycle), or failures as types (java-exception-design).4---56# Failure Models78## Purpose910Fix the fault model in writing before designing anything else. Every downstream decision —11whether a retry is safe, whether a read may be stale, how many replicas are enough — is an12answer to "which faults do we tolerate?", and a design that never asked the question has13answered it by accident.1415The failure this prevents is the one-word fault model. "The service can go down" is not a16model: it silently assumes crash-stop, which lets a developer write recovery code that is17not idempotent, treat a timeout as a definite failure, and count three replicas on one host18as three. Naming the class turns each of those into a visible, arguable claim.1920## Workflow21221. **Write a fault-model card for each boundary.** Name the fault classes, failure domains,23 synchrony assumption, recovery source, detection mechanism, and maximum tolerated24 combination. Crash-stop, crash-recovery, omission, timing and Byzantine are not labels25 for the whole system: a trusted database replica may be crash-recovery while an26 Internet-facing client is arbitrary or hostile. A process that restarts from durable27 state is crash-recovery; any in-flight operation whose completion was not durably28 recorded must be retried, reconciled, or abandoned by an explicit rule.292. **Give every remote call three outcomes**, not two: success, definite failure, unknown.30 Definite failure means the request provably never applied; anything else — read timeout,31 reset after the bytes went out, a broker ack that never arrived — is unknown. Then decide32 per call what happens on unknown: retry (safe only if idempotent), reconcile later, or33 escalate to a human. "Retry and hope" is a decision too; make it explicit. See34 `references/the-unknown-outcome.md`.353. **Add gray failure to the model.** Assume a node that is up, passing its health check, and36 answering at ten times its normal latency. If the design has no answer for that node, it37 has no answer in production either.384. **Draw the failure domains.** For a process, a host, a rack, an AZ, a dependency and a39 deploy, write what each one takes down. Replicas sharing a domain fail together for that40 cause; they may still tolerate independent process faults.415. **Do conditional availability arithmetic** on the request path before promising a42 number. Required dependencies in series multiply availability only when their events are43 independent and their SLI windows and success definitions align; genuinely independent44 redundant alternatives multiply _unavailability_. Correlated and conditional failure45 needs a measured joint distribution or an explicit common-cause model. See46 `references/failure-domains-and-arithmetic.md`.476. **Walk the eight fallacies as a checklist** — reliable network, zero latency, infinite48 bandwidth, secure network, unchanging topology, one administrator, zero transport cost,49 homogeneous network. Each is checkable in code, not just prose: a client with no read50 timeout has asserted the first, an unbounded in-memory queue the third, a hostname51 resolved once at startup the fifth.5253Inspect the actual client/driver, retry, durability and deployment configuration before assigning54outcomes. The conceptual Java type uses sealed classes/records (Java 17); exhaustive pattern55switch without preview requires Java 21. Preserve the target. Deliver the boundary's card,56evidence versus assumptions, unresolved outcome policy and one fault-injection case with an57observable invariant. Missing protocol or topology evidence means a conditional claim, not a58replica count or availability promise.5960## Fault classes6162```text63Assume crash-stop when:64- the algorithm may treat a stopped participant as never returning. Replacing its process65 identity with a fresh replica does not make the original participant crash-recovery.66Assume crash-recovery when:67- the same logical participant can return after a crash and recover durable state — local68 log, database rows, committed offsets, epochs or leases. Volatile state is lost; durable69 state may lag acknowledged work unless the durability contract proves otherwise.70Assume omission (a message or a response silently lost) when:71- the underlying transport, queue, proxy or load balancer can drop sends or receives.72 A higher-level reliable-channel abstraction may mask omissions, but its retry,73 deduplication and terminal-failure assumptions then become part of the model.74Assume timing/performance failure when:75- correctness or usefulness depends on a deadline. A correct-but-late response can be a76 failure to the caller. Allocate the caller's end-to-end deadline across attempts,77 queueing and cleanup; a timeout is not automatically useful merely because it is shorter.78Include Byzantine faults when:79- input crosses a trust boundary — a client, another tenant, a third party — where a80 participant may send arbitrary, inconsistent or hostile data. Input validation protects81 an API but does not make its replication protocol Byzantine-fault tolerant. Under common82 quorum protocols, tolerating `f` crash failures typically needs `2f+1` voting members and83 Byzantine agreement commonly needs `3f+1`; the exact bound depends on synchrony,84 authentication, quorum and protocol assumptions. State those assumptions instead of85 transplanting a replica count.86```8788## Fault-model card8990For every important operation, make these fields reviewable:9192| Field | Question that must have an answer |93| ------------------ | ---------------------------------------------------------------------------------------------------- |94| Safety invariant | What must remain true even during a partition, retry or recovery? |95| Liveness condition | Under which timing and quorum assumptions must progress resume? |96| Faults tolerated | Crash-stop, crash-recovery, send/receive omission, delay, corruption, arbitrary peer? |97| Bound | How many simultaneous faults, and in which independent domains? |98| Detector | Timeout, lease, heartbeat, quorum observation, operator signal? Which false suspicion is acceptable? |99| Durable truth | Which log, row, offset, epoch or manifest reconstructs state after restart? |100| Ambiguous effect | How is an unknown outcome deduplicated, queried, reconciled or escalated? |101| Recovery objective | What RTO/RPO and backlog-drain time are required, and under what load? |102| Re-entry | How is a recovered or partitioned participant fenced before it can mutate state again? |103104Do not merge **fault**, **error** and **failure**. A fault is the hypothesised cause; an error105is incorrect internal state; a failure is externally visible deviation from the service106contract. The distinction prevents a host reboot from being counted as one customer-visible107failure per request and prevents a latency SLO failure from being dismissed because every108response was eventually correct.109110## Rules111112- **Partial failure creates outcome uncertainty.** A local exception does not imply rollback113 either; remote calls additionally decouple caller observation from peer execution. A remote114 call can leave you not knowing — and that third outcome is where many115 distributed bugs arise. Code that maps a timeout onto "it failed" has erased it: a timeout116 states the _caller's_ patience, never the callee's state, and the callee may complete the117 work after the caller gave up.118- Crash-recovery makes the recovery path a correctness surface. For each recovery step,119 state whether it is idempotent and under which key. The mechanics are `idempotency`; the120 requirement to have an answer is here.121- **A slow node can be more damaging than a dead one.** A definitively stopped endpoint is122 eventually excluded; a slow endpoint may retain traffic and consume caller threads,123 connections and deadline budget. But aggressive suspicion can eject a healthy node and124 destroy quorum or capacity. That the system's view of health can differ from the client's125 — _differential observability_ — is why "fail fast" is a policy, not a fact. In a fully126 asynchronous network a detector cannot distinguish crash from unbounded delay; practical127 systems assume some eventual timing bound and trade false suspicion against detection128 delay. Record that trade-off for readiness checks, leases and failover.129- **Redundancy must name the fault it tolerates.** Three replicas on one host cannot survive130 losing that host, but may tolerate an individual process crash. Ask what they share: host, rack, AZ, control plane,131 image, config, deploy, certificate, downstream dependency. A shared deploy is the one most132 often missed — rolling one bad artefact to every replica introduces a common cause, which133 makes deploy strategy an availability control rather than a release convenience.134- **Adding a required dependency multiplies availability under an independence model and135 therefore increases total unavailability.** Ten independent dependencies at 99.9% in136 series produce about 99.0% path availability. For a time-based SLI on a 365-day year,137 that corresponds to roughly 87 unavailable hours, not 8.8; request failure fractions do138 not directly convert to outage hours. Real incidents are often correlated, so use this as a comparison model, not a139 forecast. Either define a tested degraded mode that removes the dependency from the140 required path, or stop quoting the higher number. Redundant alternatives multiply141 unavailability only under independence; common causes set an unavailability floor.142143## Decision framework144145```text146If the operation crosses a process boundary:147 classify timeout/cancellation/disconnect as Unknown unless protocol evidence proves148 the request could not have applied.149150If progress requires suspecting a peer:151 preserve safety with quorum, epochs or fencing;152 tune the detector only for liveness and recovery speed.153154If replicas share any host, zone, control plane, deploy, credential or dependency:155 model that cause once as a common failure domain;156 do not multiply replica availability as if independent.157158If a recovered participant can still write:159 require a new epoch/term/fencing token or an authoritative ownership check before re-entry.160161If the design claims availability during partition:162 state which operations remain safe, which side may progress, and what reconciliation163 occurs after healing. "The service stays up" is not a consistency contract.164```165166## Failure injection and recovery proof167168Test the model, not merely exception handlers:169170- inject loss separately before send, after apply/before acknowledgement, and during171 response transfer; assert downstream state and duplicate count;172- pause a process and add latency/jitter rather than testing only clean termination;173- partition asymmetrically (`A` reaches `B`, `B` cannot reach `A`) and isolate data plane174 from control plane;175- crash after every durable-write boundary, restart from persisted state, and verify the176 safety invariant plus bounded recovery;177- expire credentials, deploy incompatible versions, exhaust pools/disk/file descriptors,178 and restore backups into an isolated environment;179- run at realistic load: failover that takes 20 seconds when idle can create hours of180 recovery backlog at saturation.181182Observe detection latency, false-positive rate, unknown outcomes, duplicate/reconciliation183counts, quorum loss, recovery backlog, RTO and recovered data point (RPO). A test that only184asserts the client exception does not validate the distributed outcome.185186## Anti-patterns187188| Anti-pattern | Why dangerous / symptom | Better alternative |189| ------------------------------------ | ----------------------------------------------------------------------- | --------------------------------------------------------------------------- |190| One fault model for the whole system | Trust and durability assumptions leak across boundaries | Model each operation and participant role, then compose them |191| Timeout means rollback | Retried writes duplicate after lost acknowledgements | Preserve `Unknown`; use idempotency, status lookup or reconciliation |192| Health check means truth | Gray/asymmetric failures stay green or healthy nodes flap | Compare client-view signals; define detector error costs |193| Replica count means availability | Common deploy, zone or datastore defeats all replicas | Draw domains and measure joint/common-cause failures |194| Failover equals recovery | Traffic moves but stale owners write, data is missing, backlog explodes | Fence old owners; prove state recovery and capacity during catch-up |195| Chaos without invariants | Generates outages but no falsifiable learning | Declare safety/liveness hypotheses, blast radius and abort conditions first |196197## References198199- [The unknown outcome](references/the-unknown-outcome.md) — the three-outcome model in Java,200 how a JDBC, HTTP and Kafka call each maps onto it, and what an unknown write forces the201 design to provide. Read when adding a retry, handling a timeout, or writing across a202 process boundary.203- [Failure domains and availability arithmetic](references/failure-domains-and-arithmetic.md)204 — series and parallel composition worked through, correlated failure, and the questions205 that expose a hidden shared dependency. Read when promising an availability number, sizing206 replicas, or reviewing a topology.