TL;DR. Después de integrar más de 15 pasarelas en producción, este es el blueprint que uso para diseñar sistemas de pagos multi-gateway desde el día uno. Seis capas (Domain, Gateway Adapters, Capability Registry, Routing, Orchestration, Reconciliation), idempotency obligatoria, comunicación por eventos, y una estrategia de migración strangler-fig para sistemas legacy. Si tuviera que arrancar hoy un sistema de pagos, así lo haría.
Introducción: por qué casi todos los sistemas de pagos terminan siendo un infierno
Llevo varios años trabajando con sistemas de pagos. He integrado más de 15 pasarelas distintas — Stripe, PayPal, Adyen, CardConnect, Xendit, Razorpay, BAC, OnePay, PixelPay y un largo etcétera. Y si algo aprendí es esto:
Casi nadie diseña la arquitectura de un sistema de pagos antes de necesitarla. Se va añadiendo un gateway, luego otro, luego otro, hasta que el código se vuelve inmantenible.
El patrón que veo repetirse en empresa tras empresa es siempre el mismo: el primer gateway se integra “rápido y sucio” porque hay que salir a producción. El segundo se copia del primero. El tercero ya empieza a tener sus condicionales especiales. Y para el quinto, agregar un gateway nuevo es un proyecto de 3 meses que toca 15 archivos distintos.
Yo mismo viví eso. Y tras varias rondas de refactor — algunos exitosos, otros que tuve que rehacer — fui consolidando un blueprint mental de cómo debería diseñarse un sistema de pagos multi-gateway desde el día uno. Este post es ese blueprint.
No es una receta universal. Es lo que yo haría si arrancara hoy un sistema de pagos que tiene que soportar múltiples pasarelas, múltiples países, múltiples métodos de pago, y crecer sin colapsar. Si llegaste tarde y ya tienes un sistema hecho un desastre, también te sirve: las últimas secciones cubren la estrategia de migración.
Está dirigido a arquitectos, tech leads y devs senior que tienen que tomar decisiones de diseño. No voy a entrar en detalles específicos de Stripe vs Adyen — voy a hablar de conceptos generalizables que aplican sin importar el stack, el lenguaje o la región.
Vamos.
Parte I — Fundamentos
1. ¿Por qué multi-gateway no es opcional?
Cuando alguien me dice “para qué tantos gateways, con uno basta”, mi respuesta siempre incluye estos cinco puntos:
-
🌍 Cobertura geográfica. No existe un único PSP que cubra el mundo entero con buen pricing. Stripe es excelente en US/EU pero en Latinoamérica deja huecos. En India, RBI exige procesadores locales. En Brasil, Pix cambió las reglas del juego.
-
💸 Tarifas competitivas. Tener dos PSPs en paralelo te da poder de negociación. Le dices a Stripe “Adyen me cobra X” y de repente bajan su fee. Si solo tienes uno, estás a merced de su pricing.
-
🛡️ Resiliencia. Los PSPs se caen. Stripe ha tenido incidentes globales de varias horas. Si el 100% de tu revenue pasa por un solo gateway, una caída del PSP es una caída tuya. Con failover bien diseñado, los usuarios ni se enteran.
-
🏦 Métodos de pago locales. Pix en Brasil, OXXO en México, iDEAL en Holanda, UPI en India, SEPA en Europa. Cada región tiene su método preferido — y los locales suelen tener mejor conversión y menores fees que las tarjetas.
-
⚖️ Regulación. PSD2 + SCA en Europa, RBI en India, Open Finance en Brasil. Cada año aparecen nuevas exigencias. Estar atado a un solo PSP te deja a su ritmo de implementación.
He visto a empresas perder el equivalente a cientos de miles de dólares en una sola tarde por depender de un único gateway que se cayó. La pregunta no es si va a pasar; es cuándo.
2. Los 7 anti-patrones que matan a un sistema multi-gateway
Antes de hablar de cómo hacerlo bien, déjame mostrarte qué he visto repetirse en cada sistema que tuve que rescatar. Si alguno de estos te suena familiar, ya sabes por dónde empezar:
-
God-model
Payment. Un único modelo de 1500+ líneas que mezcla ledger, callbacks, cobros, refunds, cuentas por cobrar, créditos, eventos y notificaciones realtime. Cualquier cambio toca a 30 personas. -
HTTP dentro de transacciones de base de datos. Un
before_createo untransaction do ... gateway.charge ... endmantiene locks de DB abiertos durante la llamada al PSP. Cuando Rails (o tu framework) reintenta y el PSP ya cobró → doble cobro real. -
Dispatch dinámico sin contrato.
"#{gateway.classify}::Charge".constantize.call. Sin capability check, sin tipado, sin observabilidad por gateway. Funciona en demo, se cae en producción. -
Webhooks sin disciplina. Sin verificación de firma, sin dedupe, sin persistencia del payload crudo. He visto pagos perdidos solo porque un crash mató al worker antes de procesar el webhook.
-
Frontend acoplado al gateway. Un
switch (gateway)de 13 ramas en el cliente. Agregar un gateway = tocar el front, el back, el mobile, y rezar. -
Procesamiento síncrono donde no debería serlo. Bloquear al usuario 30 segundos esperando que un PSP latinoamericano responda. Spoiler: timeouts, retries del usuario, doble cobro.
-
Una sola fuente de verdad. Confiar 100% en el webhook. O solo en el response síncrono. O solo en el cron de reconciliation. Spoiler: las tres mienten en algún momento. Necesitas cruzarlas.
Si tu sistema actual tiene 3 o más de estos, no estás solo. Lo que sigue es cómo no caer en ellos.
3. Arquitectura por capas — el blueprint base
La base de todo es separar responsabilidades en capas claras, donde cada capa solo conoce la inmediatamente inferior. Esto es lo que llamo el blueprint base — funciona en cualquier lenguaje, en cualquier framework.
graph TB
subgraph "Presentation"
C[Controllers / GraphQL / REST endpoints]
end
subgraph "Public API (Facade)"
F[Payments::Api]
end
subgraph "Application (Use Cases)"
UC[CreatePaymentIntent · Capture · Refund · Void · ProcessWebhook]
end
subgraph "Domain"
D[PaymentIntent · PaymentSession · PaymentTransaction · WebhookEvent]
end
subgraph "Infrastructure Adapters"
A[StripeAdapter · AdyenAdapter · LocalPSPAdapter ...]
end
subgraph "Integration / Messaging"
I[Webhooks · WebSockets · Listeners · Outbox · Pub/Sub]
end
C --> F --> UC --> D
UC --> A
UC --> I
I --> UC
Las 6 capas que separo siempre:
| Capa | Responsabilidad | Lo que NO hace |
|---|---|---|
| Presentation | Cargar recursos, autorizar, delegar al facade, renderizar | Lógica de pricing, selección de gateway, llamadas HTTP al PSP |
| Public API (Facade) | Único entry point al dominio de pagos. Retorna resultados tipados | Permitir que código externo toque internals |
| Application (Use Cases) | Orquestar el flujo: idempotency → gateway selection → adapter call → persistir | Hablar HTTP directamente; saltarse el dominio |
| Domain | Entidades, state machines, invariantes financieras | Conocer HTTP, SDKs o adapters concretos |
| Infrastructure Adapters | Un adapter por gateway. Habla HTTP/SDK. Normaliza errores | Tocar el ledger; conocer el dominio de negocio |
| Integration / Messaging | Webhooks entrantes, sockets, eventos de dominio, outbox | Lógica de negocio; persistencia financiera |
La regla de oro es: el dominio no sabe que existe Stripe. El controller no sabe que existe Adyen. Toda la complejidad gateway-específica vive en los adapters.
Parte II — Las abstracciones core
4. Las abstracciones del dominio que no pueden faltar
Estas son las 9 abstracciones que aparecen en todo sistema de pagos serio que he construido o auditado. Si alguna falta, hay un dolor garantizado más adelante:
-
PaymentIntent— la intención de negocio. Es lo que el usuario quiere lograr: cobrar X monto por Y producto. Tiene una state machine explícita (created → authorized → captured → refunded) e invariantes financieras. Es la unidad de trabajo del dominio. -
PaymentTransaction— el log técnico append-only, una fila por cada llamada al PSP (authorize, capture, refund, void, tokenize). Es donde reconciliation va a buscar la verdad cuando el PSP y tu ledger no coinciden. -
PaymentSession— la sesión PSP-hosted. Cuando usas Drop-in, hosted fields, 3DS challenge o redirect, el PSP te devuelve unsession.idcon TTL. Eso no pertenece alPaymentIntent— vive aparte. -
PaymentMethod/Token— el método de pago tokenizado. Nunca guardes PAN/CVV. Guarda el token del PSP + metadata (last4, brand, exp). -
WebhookEvent— el evento crudo del PSP, persistido antes de procesar. Incluye payload, headers, IP, firma verificada y unUNIQUE(gateway, event_id)que garantiza dedupe a nivel DB. -
IdempotencyRecord— el recovery point. No alcanza con unIdempotency-Key; necesitas saber en qué paso del flujo estás para no repetir el cobro. -
GatewayRegistry+CapabilityRegistry— el catálogo de gateways y qué puede hacer cada uno. El dominio consulta capacidades antes de actuar. -
OperationResult— éxito o fracaso tipado.CardDeclined,RequiresAction,InsufficientFundsson resultados, no excepciones. Las excepciones quedan para bugs reales. -
OutboxEntry— el patrón outbox. Garantiza que un evento de dominio se publique exactamente cuando la transacción de DB se commitea, no antes, no después.
La diferencia entre un sistema que escala y uno que no es si estas 9 piezas existen como conceptos explícitos o están dispersas como columnas y banderas en un único modelo gigante.
5. Los 8 flujos canónicos (+ reconcile)
Toda operación que cualquier PSP del mundo te puede ofrecer se reduce a estos 8 flujos. Si tu sistema tiene 30 “operaciones distintas”, lo más probable es que tengas 8 mal mapeadas.
| Flujo | Qué hace |
|---|---|
tokenize |
Convertir un método de pago en un token reutilizable |
authorize |
Reservar fondos sin cobrar |
capture |
Cobrar fondos previamente autorizados |
sale |
Authorize + Capture en un solo paso |
refund_full |
Devolver el monto completo |
refund_partial |
Devolver parte del monto |
void |
Cancelar una autorización no capturada |
webhook_event |
Procesar un evento asíncrono del PSP |
Más uno extra que rara vez se modela bien:
| Flujo | Qué hace |
|---|---|
reconcile |
Cruzar el estado del PSP contra mi ledger |
Cada gateway implementa solo el subset que su capability registry declara. Por ejemplo: BAC no soporta void, Razorpay no soporta capture separado de sale. Tu dominio lo sabe vía el CapabilityRegistry y no intenta llamar a operaciones inexistentes.
Si tu equipo está debatiendo “¿esto es un capture o un sale?”, es señal de que el modelo está bien — porque la pregunta tiene una sola respuesta correcta. Si están debatiendo “¿esto es un
processPayment_v2_for_BACo unprocessPayment_alternate?”, es señal de que el modelo no existe.
6. Capability Registry — el cerebro detrás del multi-gateway
Esta es la pieza que más subestiman los equipos. El CapabilityRegistry es donde declaro qué puede hacer cada gateway:
stripe:
hosted_fields: true
3ds: true
auth_capture: true
one_step_sale: true
refund_partial: true
void: true
card_on_file: true
recurring: true
webhook: true
adyen:
hosted_fields: true
3ds: true
auth_capture: true
refund_partial: true
void: true
card_on_file: true
webhook: true
bac:
direct_pan: true
one_step_sale: true
refund_full: true
refund_partial: false
void: false
webhook: false
¿Por qué importa tanto?
Porque el dominio puede preguntar antes de actuar:
if registry.supports?(gateway, :void)
adapter_for(gateway).void(intent)
else
adapter_for(gateway).refund_full(intent)
end
Sin esto, terminas con if gateway == 'bac' regados por todo el código. Con esto, agregar un gateway nuevo es declarar sus capacidades y escribir su adapter — no tocar 15 archivos.
Es también lo que el frontend consulta vía API: “para este facility, con este método de pago, ¿qué puedo ofrecer al usuario?”. La UI se vuelve dinámica sin acoplarse a gateways específicos.
Parte III — Patrones de diseño que uso siempre
7. Patrones de diseño aplicados a payments
Voy a ser concreto: estos son los patrones que efectivamente uso en sistemas de pagos, no una lista académica de GoF. Cada uno resuelve un problema específico.
7.1 Adapter
Un contrato uniforme (GatewayAdapter), N implementaciones (StripeAdapter, AdyenAdapter…). Cada adapter expone los flujos canónicos que su gateway soporta. La inspiración clara es ActiveMerchant del mundo Ruby.
7.2 Strategy
Selección dinámica del gateway según contexto: país, moneda, método de pago, monto, hora del día. El Strategy vive en una capa por encima de los adapters y decide cuál usar.
7.3 Registry
Lookup por nombre + metadata. El GatewayRegistry resuelve "stripe" → StripeAdapter. El CapabilityRegistry resuelve ("stripe", :3ds) → true.
7.4 Factory
Construye adapters con sus dependencias inyectadas (credenciales del facility, logger, HTTP client). Centraliza la creación.
7.5 Chain of Responsibility
Un pipeline de validaciones antes de llamar al PSP: ¿tenemos credenciales? ¿el monto es válido? ¿el método de pago tiene los permisos? ¿hay anti-fraude que aprobar? Cada handler decide si continúa o aborta.
7.6 Saga / Process Manager
Para flujos largos: authorize → esperar 3DS → confirm → capture. No uso transacciones distribuidas entre mi DB y el PSP — es imposible. Modelo cada paso como una etapa de saga con su propio commit local + idempotency.
7.7 Outbox Pattern
El más subestimado de todos. Cuando creo un PaymentIntent, escribo en la misma transacción de DB:
- El
PaymentIntentcon estadoauthorized. - Una fila en
outbox_entriescon el eventoPaymentAuthorized.
Un worker independiente lee la outbox y publica al bus (Kafka, RabbitMQ, Pub/Sub). Garantía exactly-once a costa de potencialmente entregar duplicados — pero nunca de perder eventos.
7.8 Circuit Breaker
Cuando un PSP está caído, no quiero seguir mandándole requests. El circuit breaker abre el circuito tras N fallos consecutivos, deja pasar requests de prueba después de un cooldown, y se recupera solo. Sin esto, una caída del PSP te tumba a ti.
7.9 Bulkhead
Separo los workers / threads / colas por gateway. Si Xendit está lento y satura su pool, Stripe sigue funcionando. Sin bulkheading, un PSP lento contamina a todos los demás.
8. Idempotencia — la disciplina que evita el doble cobro
Si tuviera que elegir un único concepto que define la madurez de un sistema de pagos, sería este. Y casi nadie lo hace bien.
Lo que la mayoría hace: mandar un Idempotency-Key al PSP. Está bien, pero no es suficiente.
Lo que hace Brandur en su famoso post sobre Rocket Rides (te lo recomiendo si no lo has leído): cada operación de pago es una secuencia de pasos, y cada paso es un recovery point. Si crasheo en el paso 3, al reintentar arranco desde el paso 3 — no desde cero.
Pseudo-flujo:
1. CreatePaymentIntent → recovery_point = 'intent_created'
2. CallGatewayAuthorize → recovery_point = 'authorized'
3. PersistTransactionLog → recovery_point = 'logged'
4. PublishEvent → recovery_point = 'published'
5. Done → recovery_point = 'done'
Si crasheo entre 2 y 3, el reintento ve authorized y no vuelve a llamar al PSP. Solo persiste el log y publica el evento.
La regla de oro: nunca llamar al PSP dentro de una transacción de DB. La secuencia es:
- Persistir el intento + idempotency key (commit).
- Llamar al PSP (sin transacción abierta).
- Persistir el resultado + actualizar recovery point (commit).
El escenario más peligroso es el timeout del PSP. Si el PSP no responde, no sabes si cobró o no. La única salida segura: dedicar el siguiente reintento a consultar el estado (no a reintentar el cobro). Aquí es donde el Idempotency-Key te salva — al consultar, el PSP devuelve el resultado de la primera llamada.
Idempotency vive a tres niveles: en el cliente (no doble click), en tu API (no doble request del cliente), y en el adapter (no doble llamada al PSP).
Parte IV — Comunicación con el mundo exterior
9. Webhooks — la pieza más subestimada
Los webhooks son donde más he visto perder pagos. Tres reglas innegociables:
9.1 Persistir el payload crudo ANTES de procesar
La secuencia que uso:
- Recibo el HTTP request del PSP.
- Verifico firma (timing-safe —
==no sirve, leak de side-channel). - Persisto
WebhookEventcon raw payload, headers, IP,signature_verified, y unUNIQUE(gateway, event_id). - Encolo job de procesamiento.
- Respondo
200 OKal PSP. - El worker procesa el evento desde la tabla, no desde el request HTTP.
¿Por qué? Si el worker crashea, el evento sigue en la DB. Si el PSP reenvía, el UNIQUE lo deduplica. Si necesito debuggear, el raw payload está ahí.
9.2 Responder 200 OK antes de procesar
Muchos PSPs reintentan agresivamente si tardas más de 5 segundos. No proceses el evento de forma síncrona. Persiste, encola, responde 200, y procesa async. Si después falla, lo retomas — ya tienes el raw.
9.3 Orden de eventos
Los webhooks no llegan en orden. Puedes recibir payment.refunded antes que payment.captured por race conditions del PSP. Tu state machine debe ser tolerante: si llega refunded y el intent está en authorized, lo dejas en una cola de “pendiente de reordenar” y reintenta cuando llegue captured.
Y por favor: nunca confíes en que el webhook es la única señal. Es una señal. La verdad la define reconciliation.
10. Sockets y comunicación en tiempo real
Aquí entran flujos que muchos olvidan diseñar. Cuando un pago es asíncrono (Xendit, 3DS con redirect, Pix con QR), el usuario está mirando una pantalla esperando el resultado. ¿Cómo le aviso cuándo termina?
10.1 WebSockets
Conexión bidireccional persistente. Útil cuando necesitas push del backend al cliente. El patrón típico:
- El cliente abre socket y se subscribe a
payment_intent.{id}. - Backend recibe webhook → procesa → publica evento interno.
- Un listener escucha el evento y emite por el socket al cliente subscrito.
- El cliente recibe el evento y actualiza la UI.
10.2 Server-Sent Events (SSE)
Más simple que WebSocket si solo necesitas comunicación del backend al cliente. HTTP plano, reconnect automático, fácil de operar. Lo uso cuando el flujo es solo “esperar resultado de pago” y no requiere bidirección.
10.3 Long polling
Cuando WebSocket no es viable (corporate proxies, ambientes legacy). El cliente hace request, el backend lo retiene hasta tener resultado o timeout, y responde. El cliente repite. Sirve, pero escala peor.
10.4 Push via PubSub provider (Pusher, Ably, Pub/Sub)
Para mobile + multi-cliente. El cliente se subscribe a un canal y el backend publica al canal. Manejas menos infraestructura propia.
Tabla comparativa rápida
| Mecanismo | Bidireccional | Operativamente | Cuándo lo uso |
|---|---|---|---|
| WebSocket | Sí | Complejo (sticky sessions, scale-out) | Apps con muchas interacciones realtime |
| SSE | No (solo server→client) | Simple | Notificación de resultado de pago |
| Long polling | No | Sencillo, mal performance | Ambientes con restricciones de red |
| PubSub provider | Sí | Tercerizado | Mobile + multi-device |
11. Listeners y arquitectura event-driven
Cuando un pago se autoriza, suelen pasar muchas cosas: enviar email, actualizar inventario, notificar al CRM, gatillar workflows de loyalty, escribir al data warehouse. Si lo haces todo síncrono en el use case, el código se vuelve un monstruo.
La solución es publicar eventos de dominio y dejar que listeners reaccionen.
11.1 Domain events vs Integration events
Esta diferencia casi nadie la respeta y es crítica:
- Domain events — internos a tu sistema. Ejemplo:
PaymentAuthorized. Los publicas en proceso o en un bus interno. Los consume tu propio código. - Integration events — para sistemas externos. Ejemplo:
customer.charge.completed. Los publicas a Kafka/Pub/Sub. Los consume otro servicio o equipo.
Mezclarlos es un error: cambias un domain event interno y rompes un consumidor externo del que no sabías.
11.2 Listeners idempotentes
Los eventos se reentregan. Tu listener debe ser idempotente. Si recibe el mismo evento dos veces, no manda dos emails, no carga dos veces el saldo. Esto se logra con un processed_events table o con un check explícito antes de actuar.
11.3 Event sourcing parcial
No hago event sourcing completo en payments (es overkill y complica la consistencia financiera), pero sí uso PaymentTransaction como log de eventos: una fila append-only por cada acción significativa. El estado del PaymentIntent se deriva del último estado válido, pero la historia está intacta.
11.4 Cómo evito que se vuelva un caos
- Registro explícito de listeners. Nada de auto-discovery mágico.
- Naming convention:
On<Event>(e.g.OnPaymentAuthorized::SendReceiptEmail). - Un listener por archivo, un test por listener.
- Lista única de eventos del dominio, documentada como contrato.
12. Polling — cuando no queda otra
Algunos PSPs (especialmente en mercados emergentes) no envían webhooks confiables o los envían con horas de retraso. Para estos casos, polling es la salida pragmática.
Estrategia:
- Crear el intent.
- Encolar un job que consulta el estado del PSP.
- Si no hay resolución, reencolar con backoff exponencial (1s, 5s, 30s, 2min, 10min…).
- Cortar a un timeout sensato (24h o lo que dicte el PSP).
- Si pasa el timeout sin resolución, el intent va a
expiredy reconciliation lo detecta.
Polling no reemplaza webhooks ni reconciliation. Es una tercera señal. Las tres se complementan.
Parte V — Estrategias operativas y de resiliencia
13. Retry, failover y backoff
Aquí distingo cuatro estrategias que no son intercambiables:
| Estrategia | Cuándo aplica | Riesgo |
|---|---|---|
| Retry inmediato | Timeout puro, network blip | Muy bajo |
| Retry con backoff | 5xx del PSP, rate limit | Bajo |
| Failover a otro gateway | PSP completamente caído | Alto — riesgo de doble cobro |
| No retry | 4xx del usuario (CardDeclined, InsufficientFunds) | N/A — sería gastar fees |
Backoff exponencial con jitter. Sin jitter, todos tus workers reintentan al mismo tiempo y golpean al PSP justo cuando empieza a recuperarse. El jitter (variación aleatoria) los espacia.
Failover entre gateways es la estrategia más peligrosa. Si el PSP A ya cobró pero respondió con timeout, y haces failover a B, acabas de cobrar dos veces. Solo es seguro si:
- A definitivamente no cobró (respuesta explícita de error sin transacción persistida).
- O tienes un mecanismo posterior de reconciliation y refund automático.
Dead Letter Queue (DLQ). Todo lo que no se resuelve tras N reintentos va a la DLQ. Una persona revisa diariamente. Sin esto, los pagos fallidos se acumulan en silencio.
14. Selección dinámica de gateway (routing)
Routing es decidir, en tiempo real, a qué PSP mando este pago. Las estrategias que uso:
- Routing por geografía/moneda — pagos en BRL van a un PSP local; pagos en EUR van a Adyen.
- Least-cost routing — entre los PSPs habilitados, elijo el más barato para ese tipo de transacción.
- Capability-based routing — si necesito 3DS y el PSP A no lo soporta, voy al B.
- A/B routing — el 10% del tráfico va a un PSP nuevo para validarlo en producción.
- Health-check routing — si el circuit breaker de un PSP está abierto, lo excluyo automáticamente.
El routing debe ser observable: tengo que saber, para cada pago, por qué eligió el gateway que eligió. Un log estructurado con routing_reason me ha salvado de varios incidentes.
15. Reconciliation — la verdad cruzada
Reconciliation es la red de seguridad del sistema entero. Es lo que detecta cuándo tu ledger y el PSP no coinciden. Y casi siempre va a coincidir 99% — pero ese 1% es donde está el dinero perdido.
La idea es simple: descargar diariamente el extracto del PSP (settlements, payouts) y compararlo contra tu PaymentTransaction log. Tres outcomes posibles:
- Coinciden → todo bien, marca como reconciliado.
- El PSP cobró pero no tienes registro → ghost charge. Investiga.
- Tienes registro pero el PSP no cobró → fantasma local. Posiblemente intent abandonado.
Mi recomendación cuando arrancas con un sistema legacy: la primera capacidad de la nueva arquitectura que entregas a producción es reconciliation read-only. ¿Por qué?
- Blast radius cero — no escribe nada.
- Valor inmediato a Finance Ops.
- Te enseña cómo es el modelo real de eventos del PSP sin riesgo.
- Te da una base de verdad para todo lo que viene después.
Schema gateway-agnostic + adapter por PSP. Agregar el segundo gateway a reconciliation es escribir un adapter, no migrar un schema.
16. Observabilidad — logs, métricas, traces, alertas
En payments, “no sé qué pasó con ese pago” no es una respuesta aceptable. La observabilidad mínima:
-
Logs estructurados con
correlation_id— todo lo que tocó ese pago (controller → use case → adapter → webhook → listener) comparte el mismo ID. Trazar un pago de punta a punta es un solo query. - Métricas críticas por gateway:
- Success rate (target > 95%).
- Latencia P95/P99 (alerta si crece 50%).
- Retry rate (si sube, algo está mal).
- Webhook lag (tiempo entre evento del PSP y procesamiento).
-
Distributed tracing (OpenTelemetry, Datadog APM) — sigue un pago desde el click del usuario hasta el settlement bancario.
-
Alertas con criterio. Alarmar por success rate < 90% sostenido 5 min sí. Alarmar por un pago falló no. Aprende a calibrar; alertas mal calibradas matan equipos.
- Dashboard por gateway + dashboard global. Cuando algo se rompe, el primer instinto es “¿es uno o son todos?”.
Parte VI — Seguridad y compliance
17. PCI, tokenización y vault
PCI-DSS es el marco regulatorio de tarjetas. Determina qué tan auditado tiene que estar tu sistema. La diferencia entre SAQ A (mínimo) y SAQ D (máximo) son cientos de miles de dólares al año en compliance.
La regla que sigo: nunca toco PAN/CVV en mi infraestructura. Uso:
- Hosted fields / iframes del PSP — el usuario teclea la tarjeta dentro de un iframe del PSP. Tú nunca ves los datos.
- Drop-in UI — el PSP te da un componente UI completo. Aún más simple.
- Tokenización en el cliente — SDK del PSP convierte la tarjeta en un token antes de tocar tu servidor.
¿Vault propio? Casi nunca vale la pena. El costo de mantener PCI SAQ D supera por mucho el ahorro en fees. Excepción: si tu negocio es procesar pagos a escala masiva y los fees del vault del PSP son prohibitivos.
Network tokens son el futuro cercano. En vez de tokens propietarios del PSP, las redes (Visa, Mastercard) emiten tokens que sobreviven a re-emisiones de tarjeta. Tu rate de aprobación sube.
3DS / SCA — necesario en Europa por PSD2, opcional en otras regiones. Mi recomendación: integralo desde el inicio aunque tu mercado no lo exija — luego no quieres retrofittearlo.
18. Seguridad operativa
Más allá de PCI, hay capas de seguridad operativa que muchos olvidan:
-
Rate limiting — por IP, por user, por método de pago. Sin esto, eres víctima fácil de card-testing attacks (bots probando tarjetas robadas en tu sistema).
-
Card-testing detection — patrones como muchos rechazos seguidos desde la misma IP, montos chiquitos consecutivos, tarjetas con BIN sospechoso. Modelo simple → alerta → bloqueo temporal.
-
Honeypots — endpoints decoy, campos anti-bot en formularios, partial fake credentials. Detectan probing sin alertar al atacante. Es seguridad observability low-interaction — no bloqueas, observas.
-
Rotación de credenciales del PSP — calendarizada, automatizada, sin downtime. Si un secret leakea, debe ser cambiable en minutos.
-
Auditoría de accesos — quién consulta qué método de pago de qué usuario. Log inmutable.
Parte VII — Frontend en sistemas multi-gateway
19. Cómo no acoplar tu UI al gateway
El frontend es donde más he visto sistemas multi-gateway colapsar. El típico patrón malo: if (gateway === 'stripe') { ... } else if (gateway === 'adyen') { ... } por todas partes.
Lo que hago en su lugar:
19.1 Plugin pattern
La UI emite eventos canónicos, no eventos por gateway. Los eventos son del estilo payment:requires_action, payment:authorized, payment:failed. El plugin (que internamente sí conoce al gateway) traduce los eventos del SDK del PSP a este vocabulario común.
El resto del frontend escucha solo los eventos canónicos. Agregar un gateway nuevo = escribir un plugin nuevo. No tocar el resto.
19.2 Hosted fields / iframes del PSP
Reducen el PCI scope a casi nada. Trade-off: pierdes algo de control de UX. En 9 de cada 10 casos vale la pena.
19.3 Capability-driven UI
El frontend consulta a la API: “para este facility + método + monto, ¿qué puedo ofrecer?”. El backend responde con las capacidades aplicables. La UI se renderiza dinámicamente.
Resultado: si mañana un facility cambia de gateway, la UI se adapta sola — sin redeploy del frontend.
19.4 Mobile
- Deep links para volver a la app tras un 3DS web.
- SDK nativo del PSP cuando exista (mejor UX).
- Fallback a web view cuando no.
- Mismo vocabulario canónico de eventos que el web.
Parte VIII — Testing, despliegue y migración
20. Testing en sistemas de pagos
No puedes confiar en tests manuales. Lo que uso:
-
Contract testing — el adapter cumple el contrato del
GatewayAdapter. Sin esto, alguien rompe la interfaz y nadie se da cuenta hasta producción. -
Fake gateways (
BogusGatewayà la ActiveMerchant) — adapter en memoria con escenarios programables:decline,requires_action,timeout,network_error. Permite testear flujos completos sin tocar al PSP real. -
Sandbox del PSP — para tests de integración nightly. Lentos pero realistas.
-
Webhook replay tests — alimento el sistema con secuencias grabadas de webhooks reales (en orden y desorden) y verifico que el state machine se comporta bien.
-
Chaos / load tests — antes de meter un PSP nuevo a producción: ¿qué pasa con 1000 webhooks/segundo? ¿qué pasa si el PSP devuelve timeouts el 20% del tiempo?
21. Estrategias de despliegue
-
Feature flags por gateway + por facility — habilito el gateway nuevo para 1 cliente, luego para 10, luego para 100. Si algo falla, revierto el flag.
-
Canary deployments — un % chico del tráfico va a la versión nueva. Observo métricas. Promuevo si todo está bien.
-
Shadow mode — el gateway nuevo procesa en paralelo al actual pero no commitea. Comparo los resultados. Valido sin riesgo financiero.
-
Strangler-fig — para sistemas legacy. Lo cubro en la siguiente sección.
22. Migración: cuando ya tienes un sistema legacy hecho un desastre
Si llegaste a este post con un sistema legacy que ya tiene 5 anti-patrones encima, no te desesperes. Yo he migrado varios. La estrategia que funciona:
22.1 No hagas un rewrite
Los rewrites de sistemas de pagos fracasan el 90% del tiempo. Demasiada lógica de negocio invisible, demasiados edge cases que solo viven en producción. No vas a poder pararlos durante 6 meses para reescribir.
22.2 Strangler-fig + nuevo gateway como piloto
La estrategia que me ha funcionado: introducir la nueva arquitectura junto con un gateway nuevo (greenfield). El gateway nuevo vive 100% en el nuevo modelo. El legacy queda intacto.
¿Por qué con uno nuevo? Porque no carga deuda histórica, no rompe paridad con clientes existentes, y te deja validar el diseño sin riesgo de regresión.
22.3 Reconciliation-first
La primera capacidad de la nueva arquitectura que entregas a producción es reconciliation read-only sobre el legacy. Cero blast radius, valor inmediato, te enseña el modelo real.
22.4 Coexistencia larga
El legacy y la nueva arquitectura van a convivir meses o años. Acéptalo desde el principio. Diseña los namespaces y las abstracciones pensando en convivencia, no en switch.
22.5 Retira el legacy cuando ya no quede nada
El último gateway legacy se migra cuando hay justificación de negocio (cambio de pricing, nueva capability requerida). No por estética arquitectónica.
Parte IX — Lo que NO debes hacer
23. Errores que cometí o vi cometer
Cierro con la lista anti-pattern definitiva. Si no haces nada de lo anterior pero al menos evitas esto, ya estás mejor que la mayoría:
-
❌ Dual-write entre dos ledgers. Mantener dos
Payment-equivalentes sincronizados durante meses introduce más bugs financieros de los que arregla. -
❌ Migrar todos los gateways al mismo tiempo. Strangler-fig siempre. Uno por uno.
-
❌ Esconder los gateways detrás de una gem/lib “mágica” que abstrae todo. Pierdes control de los flows reales, depuras a ciegas, y la magia se rompe cuando un PSP introduce un edge case nuevo.
-
❌ Tratar a los webhooks como source of truth. Son notificaciones. La verdad la define reconciliation cruzando contra el ledger del PSP.
-
❌ Hacer failover automático entre gateways sin idempotency garantizada. Riesgo de doble cobro real.
-
❌ Mantener tarjetas en tu propia DB. El costo de PCI SAQ D es brutal. Casi nunca compensa.
-
❌ Acoplar tu modelo financiero al modelo del PSP. Tu
PaymentIntentno debería tener un campostripe_payment_intent_id. Debería tener ungateway+external_idagnóstico. -
❌ No persistir el raw payload de los webhooks. Cuando algo falle en producción (y va a fallar), sin el raw payload estás a ciegas.
-
❌ Reintentar todo, incluidos los 4xx.
CardDeclinedno se reintenta.InsufficientFundsno se reintenta. Cada reintento es un fee gastado. -
❌ No tener observabilidad por gateway. Cuando tu success rate global cae al 80%, necesitas saber cuál PSP es el culpable en segundos, no en horas.
Conclusión: la arquitectura no se hace en PowerPoint, se construye iterando
Lo que leíste no es teoría. Es la suma de cicatrices de varios proyectos donde aprendí lo que NO funciona — a veces a costa de incidentes en producción, otras veces refactorizando código heredado que ya nadie entendía.
Si arrancas hoy un sistema de pagos multi-gateway, este esqueleto te va a evitar dolor en dos años:
- Separa las 6 capas desde el día uno.
- Modela las 9 abstracciones core explícitamente — no como columnas en un modelo gigante.
- Usa los 8 flujos canónicos + reconcile como vocabulario único.
- Capability registry como cerebro del multi-gateway.
- Adapters con contrato uniforme, errores normalizados en el borde.
- Idempotencia con recovery points, no solo con
Idempotency-Key. - Webhooks → persistir antes de procesar, dedupe en DB.
- Sockets / SSE para feedback realtime al usuario.
- Listeners idempotentes para mantener el código de los use cases limpio.
- Reconciliation read-only como red de seguridad.
- Observabilidad y métricas por gateway.
- PCI scope mínimo — hosted fields, jamás vault propio salvo excepción.
Si llegaste tarde y ya tienes un sistema legacy, el camino es: strangler-fig + nuevo gateway como piloto + reconciliation-first. No rewrite. No big-bang. Coexistencia larga, migración incremental.
Ningún sistema de pagos sobrevive sin disciplina arquitectónica. Y la disciplina arquitectónica se construye antes de necesitarla, no después.
El día que un PSP global se cae durante 4 horas, y tu sistema sigue funcionando porque routeó al backup automático, es el día que entiendes por qué valió la pena cada hora invertida en este esqueleto.
Referencias y fuentes primarias
Lo que cuento aquí lo aprendí en producción, pero los principios se apoyan en specs y literatura que vale la pena tener a mano:
- Stripe API Reference — Idempotent Requests — la implementación de referencia de idempotency keys en un PSP moderno.
- Adyen Documentation — Payment methods — buen ejemplo de cómo se modela el matrix de capabilities por país y método.
- EMVCo 3-D Secure Specification — la spec original de 3DS 2.x. Si vas a integrar 3DS, léela.
- PCI DSS v4.0 — alcance, requirements, y por qué la capa de Gateway Adapters reduce dramáticamente tu PCI scope.
- Brandur Leach — Implementing Stripe-like Idempotency Keys in Postgres — el post canónico sobre idempotency a nivel base de datos. Si trabajas con Postgres y pagos, es lectura obligada.
- Martin Fowler — StranglerFigApplication — la estrategia de migración que menciono en el cierre.
- Adyen Engineering — Building reliable payment systems — buena referencia sobre reconciliation y observability en producción.
Si quieres profundizar en algún punto específico de este post, escríbeme. Y si tienes un sistema de pagos legacy y no sabes por dónde empezar la migración, también — me interesa mucho ver cómo lo han estructurado otros equipos.
En el próximo post de esta serie hablaremos de cómo comunicar arquitectura de payments a stakeholders no técnicos — el arte de pasar de diagramas a decisiones de negocio.