Skip to content

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.