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:

  1. Creates an internal payment record with status processing
  2. Extracts the required data: amount, currency, order reference
  3. 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:

  1. Receives the request in the internal domain model
  2. Translates it to the format the provider expects
  3. Invokes the provider’s API
  4. 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:

  1. The provider returns a pending or processing status
  2. The payments microservice stores this intermediate status
  3. The provider processes the payment (it may take seconds or minutes)
  4. 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:

  1. Receives the HTTP request from the provider
  2. Verifies the webhook signature (to ensure it comes from the real provider)
  3. Passes the event to the ACL for translation
  4. 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 errorInternal error
card_declinedPaymentDeclined
insufficient_fundsPaymentDeclined
rate_limitProviderTemporarilyUnavailable
invalid_requestPaymentConfigurationError

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.