Background
Multiple web applications, each with its own payment integration. Different credentials, different tokenization approaches — some applications handling card data more carefully than others. And when the organization decided to onboard a new payment provider, making that change across every application independently wasn't an option anyone wanted to live with.
Each application in its own right was in-scope for PCI DSS assessment. Security controls were inconsistent. A vulnerability in any one of them created exposure across the board. The fragmented approach had become a liability on multiple fronts simultaneously.
Anil Choudhary led the architecture design and implementation of the unified payment platform.
The Challenge
Fragmented PCI DSS Scope
PCI DSS (Payment Card Industry Data Security Standard) applies to any system that handles, processes, or stores cardholder data. With each application integrating directly with the payment gateway:
- Every application was independently in-scope for PCI DSS assessment
- Security controls needed to be verified and maintained across each application separately
- A vulnerability in any one application created PCI compliance exposure
- The annual assessment covered a large and growing surface area
Reducing PCI scope was both a security objective and a compliance cost reduction.
Inconsistent Tokenization
Tokenization replaces sensitive card data with a non-sensitive token — the token can be stored and used for repeat transactions without ever handling the actual card number. Implementation varied:
- Some applications stored card tokens correctly — but generated by different systems, not portable between applications
- Others still stored partially masked card data directly in their databases
- No shared tokenization scheme meant a returning customer needed to re-enter card details on each application
No Shared Currency Handling
The applications served customers in multiple currencies. Each application handled currency conversion and display independently:
- Exchange rate data fetched separately per application, on different schedules
- Reconciliation between applications required manual cross-referencing
- Currency formatting inconsistencies caused customer confusion
- No central record of transactions across applications in a common currency
Change Propagation Problem
Any change to the payment integration — new gateway, new payment method, updated API version, security patch — required changes to every application independently:
- High risk of inconsistent rollout — some applications updated, others missed
- Testing effort multiplied by number of applications
- No single place to update credentials or configuration
Architecture: Payment Abstraction Layer
The solution was a dedicated payment service sitting between all web applications and the payment gateway. Applications never communicate with the gateway directly.
Web Application A ──┐
Web Application B ──┼──► Payment Abstraction Service ──► Payment Gateway
Web Application C ──┘ │
Token Vault
Transaction Store
Currency Service
API Design
The abstraction layer exposed a simple, consistent API to all applications:
POST /v1/payments/initiate
POST /v1/payments/capture
POST /v1/payments/refund
GET /v1/payments/{transactionId}
POST /v1/tokens/create
GET /v1/tokens/{customerId}
POST /v1/tokens/charge
All endpoints required:
- API key authentication (per-application key, scoped to minimum permissions)
- TLS 1.2+ enforced
- Request signing for mutation operations
- Idempotency key support to prevent duplicate charges on retry
Applications sent payment requests to the abstraction layer — the layer handled the actual gateway communication, error handling, retry logic, and response normalization.
Tokenization Architecture
Card tokenization was centralized in the abstraction layer using the gateway's tokenization service:
Customer enters card details
│
▼ (HTTPS — card data never touches application server)
Payment Gateway Tokenization Endpoint
│
▼
Gateway returns token (e.g., "tok_visa_4242xxxx4242")
│
▼
Abstraction Layer stores:
{
"customer_id": "cust_abc123",
"gateway_token": "tok_visa_4242xxxx4242",
"card_last4": "4242",
"card_brand": "visa",
"expiry_month": 12,
"expiry_year": 2027
}
│
▼
Application receives: customer_id only
Application stores: customer_id only
Raw card data never touched the application servers. The gateway handled the actual card number; the abstraction layer stored only the opaque token reference. PCI DSS scope dropped from every application down to the abstraction layer and gateway integration alone. That's the meaningful reduction.
A customer's token was now portable across all applications — a returning customer's saved payment method worked on any of the web applications without re-entering card details.
Secure Token-Based Transaction Processing
Charging a returning customer used the stored token:
def charge_customer(customer_id: str, amount_pence: int, currency: str,
idempotency_key: str) -> PaymentResult:
token = token_vault.get_default_token(customer_id)
if not token:
return PaymentResult(status="no_payment_method", action_required="collect_card")
charge_request = GatewayChargeRequest(
token=token.gateway_token,
amount=amount_pence,
currency=currency,
idempotency_key=idempotency_key,
metadata={
"customer_id": customer_id,
"source_application": request.app_id
}
)
gateway_response = gateway_client.charge(charge_request)
transaction = Transaction(
id=generate_transaction_id(),
customer_id=customer_id,
gateway_charge_id=gateway_response.charge_id,
amount_pence=amount_pence,
currency=currency,
status=gateway_response.status,
source_application=request.app_id,
timestamp=datetime.utcnow()
)
transaction_store.save(transaction)
return PaymentResult(
transaction_id=transaction.id,
status=gateway_response.status,
amount_display=format_currency(amount_pence, currency)
)
The idempotency key ensured that network failures or retries couldn't result in duplicate charges — a critical correctness property for payment systems.
Multi-Currency Handling
Currency conversion and display were centralized:
class CurrencyService:
def __init__(self):
self._rates_cache = {}
self._cache_time = None
def get_rate(self, from_currency: str, to_currency: str) -> Decimal:
"""Exchange rates refreshed every 4 hours from authoritative source"""
if self._cache_stale():
self._refresh_rates()
return self._rates_cache[f"{from_currency}_{to_currency}"]
def convert(self, amount: int, from_currency: str, to_currency: str) -> int:
"""Amount in minor units (pence/cents). Returns minor units in target currency."""
rate = self.get_rate(from_currency, to_currency)
return int(Decimal(amount) * rate)
def format_display(self, amount: int, currency: str) -> str:
"""Returns localized display string: £12.50, $12.50, €12,50"""
return format_currency(amount / 100, currency, locale=CURRENCY_LOCALES[currency])
All transactions were recorded in a canonical currency (USD) alongside the original transaction currency, enabling cross-application reporting and reconciliation from a single data store.
Webhook and Reconciliation
Payment gateway webhook events (payment succeeded, payment failed, refund processed, dispute opened) were received by the abstraction layer and:
- Validated (signature verification against gateway webhook secret)
- Stored in the transaction event log
- Forwarded to the originating application via its registered webhook endpoint
- Applied to update the transaction status in the transaction store
This gave applications clean, consistent event notifications without each needing to implement their own webhook handling.
Security Hardening
Beyond the tokenization architecture, additional security controls were implemented:
- Secrets management: All gateway credentials stored in Azure Key Vault, accessed via Managed Identity — no secrets in application code or configuration files
- API key rotation: Per-application API keys rotatable without downtime via key pair model (active + standby)
- TLS enforcement: Outbound connections to gateway enforced TLS 1.2+ with certificate pinning
- Request audit log: Every API call to the abstraction layer logged with caller identity, timestamp, and request/response summary (no card data)
- Rate limiting: Per-application rate limits to prevent a single application from impacting gateway capacity for others
Results
| Dimension | Before | After |
|---|---|---|
| PCI DSS scope | Every application | Abstraction layer only |
| Card data in application databases | Present in some | Zero — tokenization only |
| Token portability | None — siloed per app | Shared — one token works across all applications |
| Currency handling | Per-application | Centralized with single exchange rate source |
| Payment method change effort | All applications | Single layer change |
| Reconciliation | Manual cross-app | Centralized transaction store |
| Duplicate charge risk | Unmitigated on retry | Eliminated via idempotency keys |
| Transaction success rate | Varied | Improved — centralized retry logic and error handling |
| API credentials exposure | In application configs | Key Vault via Managed Identity |
