01Separate intent from attempts
A merchant payment can have several traceable attempts, but an unresolved attempt prevents blind rerouting.
Trade-off: Lower apparent availability during uncertainty; operational reconciliation is required.
Payments / Independent study
A timeout is not a failed payment.
Separating payment intent from provider attempts, so retries and late confirmations don't create a second charge.
Scope
Architecture proposal, OpenAPI sketch and tested browser model. No production backend or employer data.
Proposed backend: Java · Spring Boot · PostgreSQL · AWS SQS
Executable model: TypeScript
A checkout request crosses a boundary the application cannot control. If a provider times out after accepting it, blindly retrying through another provider can turn a network problem into a duplicate charge.
Give the payment intent one owner. Persist the merchant-scoped idempotency key and request fingerprint before starting a provider attempt. Treat an ambiguous response as awaiting confirmation, then use a verified webhook or reconciliation result to resolve it.
Architecture
01
Authenticate merchant; validate amount and key.
02
Own intent, attempt history and legal transitions.
03
Translate requests; classify definite vs ambiguous failures.
PostgreSQL: payment intent + request fingerprint + attempt + outbox record. Unique merchant/key constraint. Never hold a database transaction open across a provider call.
Outbox relay → SQS → attempt worker. Verified webhooks enter an inbox; reconciliation resolves missing confirmations. Each consumer deduplicates independently.
Logical boundaries, not a deployed topology. The interactive model below covers state transitions only.
Exercise the failure path
Submit a simulated CAD 75.00 payment, retry the request, then deliver its provider confirmation twice.
key: checkout-001 / event: evt-001 / currency: CAD
Ready. Submit the payment to simulate a provider timeout.
Model limits: Single payment, in-memory TypeScript model. The webhook is assumed verified. No provider calls, persistence, concurrency or signature verification.
API design
Illustrative request and response. This contract specifies one command, not the entire proposed API.
POST /v1/payments
Idempotency-Key: command_demo
{
"amountMinor": 7500,
"currency": "CAD",
"providerToken": "tok_demo"
}Response
202 Accepted
Location: /v1/payments/pay_demo
{ "id": "pay_demo", "status": "awaiting_confirmation" }Technical decisions
A merchant payment can have several traceable attempts, but an unresolved attempt prevents blind rerouting.
Trade-off: Lower apparent availability during uncertainty; operational reconciliation is required.
Reuse a provider key for the same attempt, only where the provider guarantees that behaviour. Store the relationship locally.
Trade-off: Providers have different key retention and retry contracts. An abstraction cannot erase those differences.
Keep domain, adapter and webhook modules explicit inside one Spring Boot service before introducing more deployable services.
Trade-off: Less independent scaling initially, but simpler transaction boundaries and incident investigation.
Operating the system
Alert on age of awaiting-confirmation payments, not just HTTP error rates. Route unresolved items to a reconciliation queue with evidence.
Bound retries for definitively retryable failures. Dead-letter exhaustion for review. Never replay a financial operation without checking its current state.
Verify provider signatures and replay windows before inbox insertion. Redact tokens and sensitive payloads; retain correlation IDs and audit metadata.
Outcome & limits
The model keeps the provider attempt count at one when the request is retried, applies a confirmation once, and rejects reuse of an intent with a different amount. Those are tested local invariants, not a production reliability claim.
View study sourceNext steps
Start a conversation
I'm interested in senior backend and Tech Lead roles across fintech, payments, SaaS and platform engineering in Canada.