Payment with an external provider
Payment flow with an external provider: from the orders microservice to the payment provider, passing through the ACL and callback handling.
Why external payment is a special flow
Integrating an external payment provider (Stripe, PayPal, MercadoPago) introduces challenges that don’t exist in communication between internal services:
- The provider has its own data model and API
- Response times are unpredictable
- Communication includes asynchronous callbacks (webhooks)
- The internal domain must not be contaminated with the provider’s model
This flow shows how the Anti-Corruption Layer (ACL) protects the domain and how callbacks are handled safely.
The complete flow
sequenceDiagram
participant ORD as ms-orders
participant EB as Event Bus
participant PAY as ms-payments
participant ACL as ACL (Adapter)
participant EXT as Proveedor Externo
participant WH as Webhook Handler
ORD->>EB: OrderPlaced
EB->>PAY: OrderPlaced
PAY->>PAY: Crear intención de pago
PAY->>ACL: Solicitar cobro
ACL->>ACL: Traducir al modelo del proveedor
ACL->>EXT: POST /charges (API del proveedor)
EXT-->>ACL: charge_id, status: pending
ACL-->>PAY: paymentId, status: processing
Note over EXT: El proveedor procesa el pago
EXT->>WH: Webhook: charge.completed
WH->>WH: Verificar firma del webhook
WH->>ACL: Traducir evento externo
ACL->>PAY: Pago completado
PAY->>PAY: Actualizar estado
PAY->>EB: PaymentCompleted
EB->>ORD: PaymentCompleted
ORD->>ORD: Actualizar estado de la orden
Phase 1: Initiate the payment
Payments microservice (ms-payments)
When it receives the OrderPlaced event:
- Creates an internal payment record with status
processing - Extracts the required data: amount, currency, order reference
- Invokes the ACL to request the charge from the external provider
Anti-Corruption Layer (ACL)
The ACL is the adapter between the internal domain and the external provider:
- Receives the request in the internal domain model
- Translates it to the format the provider expects
- Invokes the provider’s API
- Translates the provider’s response back to the internal model
Translation example:
Internal model:
{
"orderId": "order-456",
"amount": 150.00,
"currency": "USD",
"customerId": "cust-789"
}
Provider model (Stripe):
{
"amount": 15000,
"currency": "usd",
"metadata": { "order_id": "order-456" },
"customer": "cus_stripe_789"
}
The ACL handles the differences: the provider uses cents instead of decimals, lowercase for the currency, and has its own customer ID system.
Phase 2: Wait for the result
Many payment providers don’t confirm the charge immediately. The flow becomes asynchronous:
- The provider returns a
pendingorprocessingstatus - The payments microservice stores this intermediate status
- The provider processes the payment (it may take seconds or minutes)
- When it finishes, the provider sends a webhook to the system
Phase 3: Receive the webhook
Webhook Handler
The webhook handler is a public endpoint that receives notifications from the provider:
- Receives the HTTP request from the provider
- Verifies the webhook signature (to ensure it comes from the real provider)
- Passes the event to the ACL for translation
- The ACL converts the external event to the internal model
Signature verification
Providers sign their webhooks to prevent forgery:
// El proveedor envía un header con la firma
X-Signature: sha256=abc123...
// El handler verifica la firma usando el secret compartido
const expectedSignature = hmac(secret, requestBody);
if (signature !== expectedSignature) {
return 401; // Rechazar
}
Event translation
The ACL translates the provider’s event to the internal model:
Provider event:
{
"type": "charge.succeeded",
"data": {
"id": "ch_stripe_123",
"amount": 15000,
"currency": "usd",
"status": "succeeded",
"metadata": { "order_id": "order-456" }
}
}
Internal event:
{
"type": "PaymentCompleted",
"payload": {
"paymentId": "pay-internal-789",
"orderId": "order-456",
"amount": 150.00,
"currency": "USD",
"providerReference": "ch_stripe_123"
}
}
The ACL’s role in detail
The ACL is not just a format translator. It also encapsulates:
Provider error handling
Each provider has its own error codes. The ACL translates them into domain errors:
| Provider error | Internal error |
|---|---|
card_declined | PaymentDeclined |
insufficient_funds | PaymentDeclined |
rate_limit | ProviderTemporarilyUnavailable |
invalid_request | PaymentConfigurationError |
Retries
If the provider’s API fails due to a transient error (timeout, 503), the ACL can retry with exponential backoff. The payments microservice doesn’t need to know about this logic.
Switching providers
If you decide to switch from Stripe to another provider, only the ACL is modified. The payments microservice and the rest of the system are unaffected.
Error scenarios
Webhook doesn’t arrive
If the provider doesn’t send the webhook (or it gets lost), the system needs a reconciliation mechanism:
- A periodic job queries the provider for pending payments
- If the payment was already processed, it updates the internal status
- If too much time has passed, it marks the payment as
timeout
Duplicate webhook
Providers may send the same webhook more than once. The handler must be idempotent: if it already processed that event, it ignores it.
Rejected payment
If the provider rejects the payment, the payments microservice publishes PaymentFailed and the orders microservice runs the corresponding compensations (release inventory, notify the user).
Summary
The external payment provider flow shows how the ACL protects the domain from the peculiarities of external systems. Communication combines synchronous calls (initiating the charge) with asynchronous callbacks (webhooks). Signature verification, model translation, and provider-specific error handling are encapsulated in the ACL, allowing the rest of the system to work with a clean domain model.