Cómo migrar un sistema de pagos sin apagarlo

Migrar un monolito es un problema resuelto en los libros y sin resolver en la práctica. Los libros asumen que puedes elegir el momento. Un sistema que mueve dinero no te deja elegirlo: cada hora que está apagado son operaciones que no ocurren, clientes esperando y proveedores con los que hay que cuadrar después.
La opción del fin de semana grande —congelar, migrar, rezar— tiene un problema aritmético. Si algo sale mal a las tres de la mañana del domingo, tus dos posibilidades son terminar a ciegas o revertir una migración a medias con datos ya escritos por el camino nuevo. Ninguna es aceptable cuando lo que se escribe son saldos.
La alternativa que usamos, servicio por servicio, es shadow-first: el camino nuevo se ejecuta desde el primer día, en producción, con tráfico real, pero sin poder tocar nada.
Qué significa "sin poder tocar nada"
Shadow no es un entorno. No es staging con datos copiados. Es el código nuevo corriendo dentro de la misma petición que el viejo, sobre los mismos datos de entrada, en el mismo instante.
La diferencia es que sus efectos están cortados de raíz:
- No escribe en base de datos.
- No llama a proveedores externos.
- No emite eventos ni encola trabajos.
- Su resultado no se le devuelve a nadie.
Lo único que hace es calcular y registrar lo que habría hecho. El camino viejo sigue siendo el que responde, el que escribe y el que el cliente ve.
Y esto tiene que ser una garantía estructural, no una promesa. Aquí es donde se falla: se confía en que "el código nuevo aún no escribe" porque nadie ha añadido esa línea todavía. Basta un descuido para que un shadow empiece a emitir efectos reales. Lo que funcionó fue un guard explícito: mientras el servicio está en modo sombra, la capa de persistencia y los clientes externos rechazan cualquier operación de escritura. Si el código nuevo intenta escribir, revienta en los logs de shadow —donde no le hace daño a nadie— en vez de escribir de verdad.
Por qué en producción y no en un entorno de pruebas
Porque los datos reales son el único juez.
Un entorno de pruebas tiene los casos que alguien imaginó. Producción tiene los que nadie imaginó: la orden de 2019 con un campo que ya no existe, el proveedor que devuelve el código de error mal documentado, el usuario que tiene dos beneficiarios con el mismo nombre porque hubo un backfill imperfecto hace dos años.
Esos casos son precisamente los que rompen una migración, y no aparecen en ningún fixture. Shadow-first los encuentra durante semanas, sin coste, mientras el negocio sigue funcionando con el camino viejo.
La métrica que importa no es "los tests pasan". Es la tasa de coincidencia entre los dos caminos sobre tráfico real, y sobre todo la lista de discrepancias: cada una es un caso que no habías entendido.
El orden del cutover
- Sombra. El camino nuevo corre y registra. Nadie lo nota.
- Comparación. Se revisan las discrepancias. Cada una se explica: o es un bug del camino nuevo, o el viejo estaba haciendo algo mal que nadie había notado. Ambas cosas pasan, y la segunda es la más incómoda de encontrar.
- Flip. Una feature flag cambia quién responde. El viejo sigue ahí, intacto.
- Observación. Días, no minutos. Con el camino viejo listo para volver a tomar el mando.
- Retirada. Solo cuando nadie ha tenido que volver atrás, se borra el código viejo.
El paso 5 es el que todo el mundo se salta, y es el que hace que el sistema quede realmente limpio. Un módulo migrado que conserva su versión antigua "por si acaso" durante un año no está migrado: está duplicado, y algún día alguien arreglará un bug solo en una de las dos copias.
La disciplina que esto exige
Rutas disjuntas. El camino nuevo no puede reutilizar piezas del viejo "para ir más rápido". En cuanto comparten un servicio a mitad de camino, ya no estás comparando dos implementaciones: estás comparando una implementación consigo misma en dos disfraces, y la sombra deja de decirte nada.
Saber revertir sin drama. Nos pasó: un módulo de cara al cliente se activó, se comportó peor de lo previsto en un flujo concreto y volvió atrás en cuestión de horas. No fue un fracaso del método, fue el método funcionando. El flip es reversible por diseño; si tu cutover no se puede deshacer con un cambio de flag, no has hecho un cutover, has hecho un salto.
Aceptar que el viejo a veces tiene razón. Varias discrepancias resultaron ser comportamientos raros del sistema antiguo que el negocio ya daba por buenos. Cambiarlos "porque están mal" habría roto expectativas de clientes. Migrar no es la oportunidad de arreglar todo a la vez; es la oportunidad de mover algo sin romperlo.
Lo que shadow-first no resuelve
Sería deshonesto vender esto como una bala de plata.
No valida lo que no ejecuta. Si cortas las escrituras, no estás probando las escrituras. Los errores de persistencia, las restricciones de unicidad, los deadlocks bajo carga: nada de eso aparece en sombra. Aparece el día del flip, y hay que esperarlo.
Hay operaciones que no se pueden ejecutar dos veces. No puedes hacer sombra de un pago real: no vas a llamar al proveedor dos veces para comparar. En esos casos la sombra no compara el efecto, compara la decisión: a qué proveedor habría enrutado, con qué comisión, con qué monto. El efecto sigue sin probarse hasta el flip.
Cuesta. Estás manteniendo dos implementaciones vivas durante semanas, y pagando cómputo por trabajo que se descarta. Es un coste real, y es más barato que un descuadre.
La pregunta que hago ahora
Cuando alguien me cuenta que va a reescribir un sistema que mueve dinero, ya no pregunto por la arquitectura de destino. Pregunto:
¿Cómo vas a saber que el sistema nuevo hace lo mismo que el viejo, antes de que le toque hacerlo de verdad?
Si la respuesta es "tenemos buenos tests", la migración va a descubrir en producción todo lo que producción sabía y los tests no. Y lo va a descubrir con dinero de por medio.
Comments
No comments yet. Be the first.