ADR-0003: Phase 2 proposes a hold/capture API to the Wallet team
Status: Accepted (our side) · Date: 2026-10-19. The Wallet team's delivery is working assumption A9:
M3 is built against the stub now. Enabling Phase 2 in production is gated on contract tests against the
Wallet sandbox (architecture §10, step 1).
Decision owner: Wallet Service team lead · Interface: api/wallet-service/v2
Problem, stated as a correctness issue (not a performance request)
Phase 1 is correct, but every cancellation is a compensating credit. Money really leaves the
customer's account and comes back, which hurts customer trust and reconciliation. Card-present
payments are naturally two-step: reserve at the till, settle later. The industry-standard primitive is
a hold with a TTL. Building one is a small change for the team that owns the balance (a
reservations table and one subtraction in available), and it cannot be built by anyone else.
Proposal
POST /holds (atomic reserve or INSUFFICIENT_FUNDS), GET /holds?reference=, GET /holds/{id},
POST /holds/{id}/capture, DELETE /holds/{id}, all idempotent on Idempotency-Key.
Required invariants H1–H5 (see the spec). The critical one is H1: every debit path, legacy
included, checks available = ledger − holds. Without it, a legacy debit can consume held funds and a
capture can fail for NSF after we already approved the till.
What we do meanwhile
Ship Phase 1 (ADR-0001), which needs nothing from the Wallet team. Our client, stub and strategy for v2 are built against this spec (M3), so adoption is a configuration change (ADR-0005).
Consequences if accepted
- + Clean cancellation (release), no statement noise, decision and settlement separated.
- + A natural, side-effect-free breaker probe (
GET /holds/{id}). - − Holds need TTL discipline: hold TTL > capture retry window + reaper period.
If rejected
Phase 1 remains the steady state. It is correct, and its cost is compensating credits.