ADR-0007: Deadline budget, timeouts and retries
Status: Accepted · Date: 2026-10-19 · Resolves: OQ-1
Context
The till abandons at ~3 s. The wallet's real latency profile is not known (OQ-1). Implementers need fixed numbers and one unambiguous retry rule, otherwise every adapter invents its own timeouts and retries multiply across layers.
Decision
Working assumption A11: a healthy wallet answers POST /debits and POST /holds with p99.9 ≤ 400 ms.
Every limit below is derived from it. All limits are properties in safeledger.budget and
safeledger.wallet. Changing a number is a config change, not a design change.
| Item | Value | Rule |
|---|---|---|
| Server budget | 2,400 ms | effective = clamp(Request-Deadline-Ms − 600 ms, 0, 2,400 ms); header absent → 2,400 ms |
| Decision call (debit / hold) | 800 ms | deadline.capAt(800 ms) (2× the assumed p99.9) |
| Lookup by reference | 300 ms | deadline.capAt(300 ms) |
| Worker call (credit, capture, release, lookup) | 2 s | Deadline.in(2 s) per attempt; nobody is waiting |
| Connect timeout | 200 ms | The only fixed timeout. It is set on the HTTP client and is below every call limit |
| Minimum budget to send | 50 ms | If remaining < 50 ms before a call, do not send. Return Unavailable("deadline spent") |
Retry-After on WALLET_UNAVAILABLE / WALLET_TIMEOUT |
2 s | Fixed |
Retry-After on 409 in flight, own 503, 404 PAYMENT_NOT_YET_DECIDED |
1 s | Fixed |
Retries:
1. The decision path never retries a wallet call. A failed or unavailable decision is a retryable
decline. The till retries with a new Idempotency-Key (architecture §7).
2. An Ambiguous decision call gets exactly one lookup by reference, if remaining ≥ 50 ms.
3. All other retries happen in the work queue (ADR-0008), which owns the outbound idempotency key.
The HTTP client and Resilience4j have no retry configured.
Classification of a wallet call (identical for v1 and v2):
| Observation | Outcome |
|---|---|
| 2xx | Success |
409 DUPLICATE_REFERENCE (v1) |
Success, built from the request plus existingDebitId / existingCreditId. No extra call. |
409 INSUFFICIENT_FUNDS, HOLD_NOT_ACTIVE, HOLD_ALREADY_CAPTURED, AMOUNT_EXCEEDS_HOLD; 404; 423; other 4xx |
Rejected(code) |
503, 502, connection refused, connect timeout, breaker open, bulkhead full, remaining < 50 ms |
Unavailable (certainly not applied) |
| Request timeout after the request was written, connection reset after send, 504, other 5xx | Ambiguous |
Consequences
- + One rule set for all clients. Tests can assert exact budgets (SLOW →
Ambiguousin ≤ 800 ms). - − If A11 is false (p99.9 > 400 ms), the slow-call breaker trips on a healthy wallet. Revisit
trigger: the measured
walletclient p99.9 over 7 days exceeds 400 ms. Then retune the limits from the measurement, keepingdecision + lookup + 300 ms≤ server budget.