Commerce · 3 min read
Reference ArchitectureTransaction & Payment Integration Architecture
A reference pattern for connecting commerce-oriented applications to payment and identity providers without risking duplicate charges or spoofed events.
Reference architecture. This is a representative pattern generalized from independent integration work — not a description of a specific employer's system.
Payment integrations look simple until you account for how providers actually behave in production: retries, out-of-order webhook delivery, and the gap between "the customer clicked pay" and "the provider confirmed the charge."
The core tension
Checkout UX wants a fast, synchronous answer. Payment confirmation is often asynchronous — delivered later via a webhook. The architecture has to bridge that gap without either blocking the user or trusting unconfirmed state.
The pattern: a transaction service owns transaction state as the source of truth. It initiates a charge with the payment provider, marks the transaction pending, and returns immediately. A separate webhook handler later confirms (or rejects) that transaction once the provider reports the outcome.
Idempotency is not optional
Every payment provider retries. A network blip, a slow response, a timeout — all of these cause the provider to resend the same request or the same webhook. If the transaction service isn't idempotent, a retry becomes a duplicate charge.
The only reliable guarantee is a unique constraint at the database layer on an idempotency key supplied with the original request. Application-level checks (an in-memory cache, a pre-request lookup) can race under concurrent retries; a database constraint cannot.
Webhook signature verification is the perimeter
An unauthenticated webhook endpoint is an open door: anyone who finds the URL can POST a fake "payment succeeded" event. Every provider that sends webhooks also signs them — verifying that signature before acting on the payload is the entire security boundary for this integration point, and it should be treated as such, not as an optional hardening step added later.
Keep the webhook handler fast and dumb
Providers retry aggressively on anything but a fast 2xx response. The handler's only job should be: verify the signature, deduplicate against an idempotency key, and enqueue the actual state change. Doing real work — updating records, triggering side effects — inside the handler risks a slow response triggering more provider retries, compounding the load.
What this buys
- No duplicate charges under provider retry behavior, because idempotency is enforced at the data layer, not just in application logic.
- No spoofed state changes, because nothing updates a transaction without a verified signature.
- A clear audit trail, because every transition — pending, confirmed, failed — is a recorded state change, not an in-place mutation.
This pattern generalizes the transaction handling used in the Transaction & Commerce Integration Architecture reference system.