01 · Case study

Custom high-revenue payment platform with multi-gateway integrations

Built the company's central payment platform from scratch - multi-gateway routing, subscriptions, customer vaults, multi-domain storefronts, queue-driven webhooks, full audit history, and a REST API every other internal product wrote into so all revenue lived in one place.

Context

The platform was the company’s primary revenue path and, eventually, the system of record for every transaction in the business. Other internal products eventually pushed their sales into it through a REST API so company-wide reports stayed accurate even when the storefront and gateway lived elsewhere.

Problem

Payments started as straight gateway calls bolted onto product code. Fine with one gateway and one team. As the product grew it had to handle multiple gateways with different fees and country coverage, recurring subscriptions, deferred authorization captures, alternative payment methods (bank transfer) that don’t return synchronously, multiple storefronts on different domains, multiple languages, and centralized reporting for transactions that originated outside the platform. None of that fit the bolted-on shape, and failures were absorbed by support instead of the system.

Approach

I extracted payments into a dedicated service and built it out into a full platform:

  • Gateways behind a common interface, with idempotency keys on every request. A routing layer chose a gateway per transaction based on country, volume, and per-gateway fees, with health-based load balancing across providers.
  • Customer vaults: tokenized references stored against the customer record so subsequent charges never re-touched card data.
  • Subscriptions and scheduled work: recurring billing, retry policies for failed renewals, and configurable authorization-capture jobs (capture N days after auth) all ran on a scheduler.
  • Webhooks both directions: outbound on sale events, inbound from gateways for asynchronous methods like bank transfer, where a sale was created pending and confirmed when the processor’s webhook arrived later. Both directions ran through Redis-backed queues with retries and dead-letter handling.
  • Multi-domain storefronts: products, upsells, themes, and copy were per-domain. The same backend served multiple brands.
  • Internationalization: dynamic translations across storefronts and admin.
  • Dynamic fees: shipping, platform, and per-product fees were configurable and applied at checkout.
  • Full audit history: every change to configuration and core models was versioned, so “what did this product cost last Tuesday at 3pm” was a query, not forensics.
  • Admin dashboard: products, upsells, users, gateway configs, domains, and reports all managed from the same UI.
  • REST API for external transactions: other internal products wrote their sales in via this API so revenue reporting stayed consistent even when the checkout lived elsewhere.
  • Failed-payment handoff: workflows routed unrecoverable failures to Zendesk so support stopped being the queue.
  • Transactional email: SES and SendGrid behind a routing layer so providers could swap without code changes.
  • CI/CD: replaced the manual deploy ritual; the same pipeline later covered the rest of the platform.

Postgres was the system of record. Redis backed cache and queues.

Outcome

Duplicate-charge incidents stopped. Asynchronous payments became normal flow rather than manual reconciliation. Subscriptions and deferred captures ran on schedule rather than ad-hoc scripts. Adding a new gateway, storefront, or language became configuration, not engineering. Centralized reporting via the REST API meant every transaction in the company lived in one place. Deploys went from a senior-engineer task to a normal one, and the pattern (abstracted gateways, idempotency, queued webhooks, audited config) became the template for later integrations.