Módulo 4: Branch by abstraction
Parallel-run para probar que el cambio es seguro
Descripción
En la lección 4 montaste el flag y verificaste que conmutar la implementación no rompe a los llamadores —pero con una trampa cómoda: ModernShipping coincidía con el legacy en todos los casos, así que callers OK? daba True de gratis—. En la realidad, la implementación nueva casi nunca coincide con la vieja al primer intento. Un monolito legacy está lleno de reglas tácitas —bordes que nadie documentó, excepciones que se agregaron un martes hace cinco años— y la reimplementación, por buena que sea, se salta alguna. La pregunta central de este paso es: ¿cómo sabes que puedes confiar en la nueva antes de darle un solo usuario real? La respuesta es el parallel-run.
Un parallel-run es simple y poderoso: para cada entrada, llamas a las dos implementaciones —la vieja y la nueva—, comparas sus resultados, y reportas las discrepancias. Pero con una regla de oro: le devuelves al llamador siempre el valor del legacy, nunca el del modern. El modern corre "en la sombra": se ejecuta y se compara, pero su resultado no llega al usuario. Así puedes validar la nueva implementación contra la vieja sobre tráfico real (o sobre un lote de casos conocidos), atrapando cada diferencia, sin que una sola de esas diferencias afecte a nadie. El parallel-run es el motor nuevo del avión encendido en tierra, con sus lecturas comparadas contra el viejo, antes de confiarle un vuelo con pasajeros.
Esta lección lo ejecuta con un bug real. Montamos una ModernShippingV1 que olvidó una regla tácita del legacy —que el envío gratis no aplica a international— y corremos el parallel-run: la corrida atrapa la discrepancia en el caso international, en un pedido conocido, con el número exacto de la diferencia. Arreglamos (ModernShippingV2), volvemos a correr, y la discrepancia desaparece: 0 diferencias. Solo entonces es seguro subir el flag. El parallel-run convierte "creo que la nueva es equivalente" en "medí que la nueva es equivalente en estos casos", que es una afirmación muy distinta.
Conexión con el módulo. La lección 4 te dio el flag; esta te da lo que hay que verificar antes de subirlo. El orden real del patrón es: parallel-run limpio primero (esta lección), rollout del flag después (lección 4). La 6 borra el legacy cuando el flag llegó a 100% y el parallel-run se mantiene limpio. Fíjate en la frontera: el parallel-run de aquí es rápido y de comportamiento —compara la salida de dos implementaciones de una función, en memoria, sobre casos conocidos—. El parallel-run de datos a fondo (leer de dos almacenes durante una migración de datos, con reconciliación en producción a escala) es el módulo 6. Aquí es una herramienta de validación del swap; allá es una fase de la migración de datos. La idea es la misma —correr los dos y comparar antes de confiar—; el objeto que se compara y la escala son distintos.
Una analogía: el probador que hornea las dos recetas y compara
Imagina una panadería que quiere reemplazar su receta vieja de pan —la de la abuela, escrita a mano, con anotaciones al margen que nadie entiende del todo— por una receta nueva, más limpia y fácil de escalar. Antes de vender el pan nuevo a los clientes, el maestro panadero hace algo prudente: durante una semana, hornea las dos recetas en paralelo, la vieja y la nueva, con los mismos ingredientes y el mismo horno. Y compara los panes: ¿mismo peso?, ¿misma corteza?, ¿misma miga?, ¿mismo sabor?
Lo que vende a los clientes durante esa semana es siempre el pan viejo —el probado, el que la gente ya conoce—. El pan nuevo se hornea, se compara, y se lleva a la mesa de cata interna; no llega al mostrador. Si un día el pan nuevo sale más plano, o con la corteza pálida, el panadero lo detecta en la cata —no en la queja de un cliente—. Descubre que la receta nueva se saltó un reposo que la anotación al margen de la abuela pedía "cuando hace calor". Ajusta la receta nueva, vuelve a hornear en paralelo, y compara otra vez. Solo cuando el pan nuevo sale idéntico al viejo, día tras día, empieza a venderlo.
Hornear las dos recetas y comparar es el parallel-run. Vender siempre el pan viejo durante la prueba es la regla de oro —el modern corre en la sombra, el usuario recibe el legacy—. La anotación al margen que la receta nueva se saltó es la regla tácita del legacy que la reimplementación olvidó. Y la cata interna que atrapa el pan plano antes de que llegue a un cliente es la discrepancia que el parallel-run reporta antes de que llegue a un usuario. La panadería no adivina si la receta nueva es buena: la mide, en paralelo, con el pan viejo como referencia, sin arriesgar la clientela.
Ejemplo trabajado: el parallel-run atrapa una discrepancia y la validación se limpia
Vamos a montar el parallel-run y a usarlo para atrapar un bug de paridad. Tenemos el LegacyShipping de siempre. Y montamos una ModernShippingV1 que tiene un bug plausible: al reimplementar la regla de envío gratis, escribió if order["order_total"] >= 50.0: cost = 0.0 —olvidando el and order["zone"] != "international" del legacy—. Aisladamente, ese código parece razonable; solo el parallel-run, comparando contra el legacy, lo delata. El parallel-run llama a las dos, compara, reporta discrepancias, y devuelve siempre el legacy. Corremos con V1 (atrapa la discrepancia), arreglamos a ModernShippingV2, y volvemos a correr (0 discrepancias).
from typing import Protocol
class ShippingCalculator(Protocol):
def cost(self, order: dict) -> float: ...
class LegacyShipping:
def cost(self, order: dict) -> float:
base = {"local": 5.0, "national": 10.0, "international": 25.0}[order["zone"]]
cost = base + max(0.0, order["weight_kg"] - 1.0) * 2.0
if order["order_total"] >= 50.0 and order["zone"] != "international":
cost = 0.0
return round(cost, 2)
# --- modern v1: tiene un BUG. Olvido la regla tacita "el envio gratis NO aplica a
# international". Aisladamente parece razonable; solo el parallel-run lo delata. ---
class ModernShippingV1:
def cost(self, order: dict) -> float:
base = {"local": 5.0, "national": 10.0, "international": 25.0}[order["zone"]]
cost = base + max(0.0, order["weight_kg"] - 1.0) * 2.0
if order["order_total"] >= 50.0: # BUG: falta 'and zone != international'
cost = 0.0
return round(cost, 2)
# --- modern v2: arreglada, con la regla tacita ya replicada. ---
class ModernShippingV2:
def cost(self, order: dict) -> float:
base = {"local": 5.0, "national": 10.0, "international": 25.0}[order["zone"]]
cost = base + max(0.0, order["weight_kg"] - 1.0) * 2.0
if order["order_total"] >= 50.0 and order["zone"] != "international":
cost = 0.0
return round(cost, 2)
ORDERS = [
{"id": 1, "zone": "local", "weight_kg": 0.5, "order_total": 20.0},
{"id": 2, "zone": "national", "weight_kg": 1.0, "order_total": 30.0},
{"id": 3, "zone": "international", "weight_kg": 3.0, "order_total": 80.0},
{"id": 4, "zone": "local", "weight_kg": 1.0, "order_total": 60.0},
{"id": 5, "zone": "national", "weight_kg": 4.0, "order_total": 55.0},
]
# --- parallel-run: llama a las DOS, compara, reporta la discrepancia, pero DEVUELVE
# el valor del legacy (el usuario nunca ve el resultado de la nueva sin verificar). ---
def parallel_run(orders, legacy: ShippingCalculator, modern: ShippingCalculator):
mismatches = []
for o in orders:
lc, mc = legacy.cost(o), modern.cost(o)
if lc != mc:
mismatches.append((o["id"], o["zone"], lc, mc))
# el llamador recibe SIEMPRE el valor del legacy: parallel-run no expone la nueva.
return mismatches
legacy = LegacyShipping()
print("=== parallel-run con modern v1 (con el bug) ===")
mm = parallel_run(ORDERS, legacy, ModernShippingV1())
print(f"{'order':>6}{'zone':>16}{'legacy':>9}{'modern':>9} estado")
print("-" * 49)
for o in ORDERS:
lc, mc = legacy.cost(o), ModernShippingV1().cost(o)
flag = "OK" if lc == mc else "DISCREPANCIA"
print(f"{o['id']:>6}{o['zone']:>16}{lc:>9}{mc:>9} {flag}")
print(f"\nDiscrepancias: {len(mm)} -> {mm}")
print("NO conmutes el flag: la nueva difiere del legacy en un caso conocido.\n")
print("=== parallel-run con modern v2 (arreglada) ===")
mm2 = parallel_run(ORDERS, legacy, ModernShippingV2())
print(f"{'order':>6}{'zone':>16}{'legacy':>9}{'modern':>9} estado")
print("-" * 49)
for o in ORDERS:
lc, mc = legacy.cost(o), ModernShippingV2().cost(o)
flag = "OK" if lc == mc else "DISCREPANCIA"
print(f"{o['id']:>6}{o['zone']:>16}{lc:>9}{mc:>9} {flag}")
print(f"\nDiscrepancias: {len(mm2)} -> {mm2}")
print("0 discrepancias en los casos conocidos: ahora SI es seguro subir el flag.")
Qué esperar. Al correr el archivo, la salida es exactamente esta:
=== parallel-run con modern v1 (con el bug) ===
order zone legacy modern estado
-------------------------------------------------
1 local 5.0 5.0 OK
2 national 10.0 10.0 OK
3 international 29.0 0.0 DISCREPANCIA
4 local 0.0 0.0 OK
5 national 0.0 0.0 OK
Discrepancias: 1 -> [(3, 'international', 29.0, 0.0)]
NO conmutes el flag: la nueva difiere del legacy en un caso conocido.
=== parallel-run con modern v2 (arreglada) ===
order zone legacy modern estado
-------------------------------------------------
1 local 5.0 5.0 OK
2 national 10.0 10.0 OK
3 international 29.0 29.0 OK
4 local 0.0 0.0 OK
5 national 0.0 0.0 OK
Discrepancias: 0 -> []
0 discrepancias en los casos conocidos: ahora SI es seguro subir el flag.
Lee la primera corrida, la de ModernShippingV1. Cuatro filas dan OK y una da DISCREPANCIA: el pedido 3, international con total 80.0. El legacy cobra 29.0 (base 25.0 + recargo de 4.0 por los 2 kg extra), pero ModernShippingV1 cobra 0.0. ¿Por qué? Porque V1 olvidó la regla tácita: al superar el pedido los 50.0, V1 lo hace gratis, sin excluir international como hace el legacy. La línea de resumen lo dice exacto: Discrepancias: 1 -> [(3, 'international', 29.0, 0.0)]. Ese (3, 'international', 29.0, 0.0) es un reporte accionable: en el pedido 3, zona international, el legacy da 29.0 y el modern da 0.0. No es "algo anda mal"; es "aquí, exactamente, difieren, y por cuánto".
Y fíjate en lo que no pasó: ese pedido 3 nunca recibió 0.0 como respuesta. El parallel-run devuelve siempre el valor del legacy (29.0); el 0.0 del modern se calculó, se comparó, y se reportó —pero no llegó a ningún usuario—. Si esto fuera producción real, el cliente del pedido 3 habría pagado los 29.0 correctos, y el equipo se habría enterado del bug del modern por el reporte de discrepancias, no por un cliente enojado que recibió envío gratis indebido. Esa es la seguridad del parallel-run: valida la nueva sobre casos reales sin que sus errores lleguen a nadie.
La segunda corrida es con ModernShippingV2, donde se agregó el and order["zone"] != "international" que faltaba. Ahora las cinco filas dan OK, incluido el pedido 3 (29.0 en las dos), y el resumen dice Discrepancias: 0 -> []. Esa lista vacía es la luz verde: la implementación nueva coincide con la vieja en todos los casos conocidos. 0 discrepancias en los casos conocidos: ahora SI es seguro subir el flag. El parallel-run convirtió una corazonada ("la v1 parecía bien") en una medición (la v1 falla en international; la v2 no), y solo tras la medición limpia se autoriza el rollout del flag de la lección 4.
Profundización: la regla de oro, y qué es "seguro" y qué no
La estructura del parallel-run cabe en un diagrama:
parallel_run(order)
│
┌────────────────┴────────────────┐
▼ ▼
legacy.cost(order) modern.cost(order)
│ │
│ (se DEVUELVE al llamador) │ (se calcula, NO se devuelve)
▼ ▼
respuesta al usuario ◄── comparar ──► solo para el reporte
│
si difieren: registrar discrepancia
La regla de oro —devolver siempre el legacy— es lo que hace el parallel-run seguro. Mientras la nueva implementación no esté validada, su resultado no debe llegar a un usuario, porque podría estar mal (como el 0.0 de V1). El parallel-run la corre "en la sombra": la ejecuta para poder compararla, pero descarta su salida de cara al usuario. Esto tiene un costo —calculas dos veces— pero compra algo muy valioso: validación sobre tráfico real sin exposición. Cuando el parallel-run lleva suficiente tiempo (o suficientes casos) en 0 discrepancias, ahí empiezas a devolver el modern —que es, precisamente, subir el flag de la lección 4—.
Conviene ser preciso sobre qué demuestra y qué no un parallel-run. Demuestra paridad en los casos que corriste: si comparaste 5 casos y dieron 0 discrepancias, sabes que coinciden en esos 5. No demuestra paridad universal: podría existir un sexto caso, no cubierto, donde difieran. Por eso, en la realidad, el parallel-run se corre sobre muchos casos —idealmente sobre una muestra del tráfico real de producción, durante días— para que el "casos conocidos" se acerque a "todos los casos que de verdad ocurren". Cuantos más casos reales cubra sin discrepancias, más justificada la confianza. Cinco casos elegidos a mano, como en el ejemplo, ilustran la mecánica; una validación seria corre miles.
Y una decisión importante que el parallel-run fuerza: cuando aparece una discrepancia, ¿el legacy tiene la razón o el modern? Casi siempre el legacy —es el comportamiento actual, el que el negocio ya vive, y la regla del patrón es migrar a paridad primero—. La discrepancia de V1 (international gratis) es un caso claro: el legacy tiene razón, international no debe ser gratis, y el modern se arregla para coincidir. Pero ocasionalmente la discrepancia revela un bug del legacy que el modern "arregló" sin querer. Ahí la regla es no decidirlo en caliente: primero llevas el modern a paridad exacta con el legacy (bug incluido), completas la migración, y después, en un cambio aparte y deliberado, corriges el bug —para no mezclar "migré" con "cambié comportamiento"—. El parallel-run no decide quién tiene razón; te muestra dónde difieren para que tú lo decidas, y la decisión por defecto es paridad.
Errores comunes
Devolver el resultado del modern durante el parallel-run. Qué pasa: el equipo, con prisa por "usar ya" la implementación nueva, hace que el parallel-run devuelva el valor del modern y solo registre si difiere del legacy. Por qué pasa: parece un atajo —"si ya lo estamos calculando, ¿por qué no usarlo?"—. Cómo detectarlo: un usuario recibe un resultado de la implementación no validada; la discrepancia de V1 (0.0 en international) le habría llegado a un cliente. Cómo corregirlo: la regla de oro es innegociable —durante el parallel-run se devuelve siempre el legacy—. El modern corre en la sombra, se compara, se reporta, pero no se expone. Devolver el modern es subir el flag, y eso solo se hace tras 0 discrepancias sostenidas. Un parallel-run que devuelve el modern no es un parallel-run: es un rollout sin validación, con el nombre equivocado.
Concluir "es equivalente" con muy pocos casos. Qué pasa: el parallel-run da 0 discrepancias sobre cinco casos y el equipo declara paridad universal y sube el flag al 100%. Por qué pasa: cinco OK seguidos dan una certeza que la muestra no respalda. Cómo detectarlo: tu validación cubre un puñado de casos elegidos a mano, no la variedad del tráfico real; un pedido con una combinación rara (zona + peso + total) que no está en la muestra podría diferir. Cómo corregirlo: corre el parallel-run sobre muchos casos —idealmente una muestra del tráfico de producción, durante días— para que "casos conocidos" se parezca a "todos los casos reales". Y aun así, sube el flag gradual (lección 4): el rollout del 10% con la vieja como red es la segunda capa de seguridad por si el parallel-run no cubrió algún caso. Paridad medida sobre muchos casos + rollout gradual, no cinco casos + salto al 100%.
"Arreglar" un bug del legacy dentro de la reimplementación, sin decidirlo. Qué pasa: el parallel-run muestra una discrepancia, el equipo mira y piensa "el legacy está mal aquí, el modern lo hace bien", y deja que el modern difiera —mezclando la migración con una corrección de comportamiento—. Por qué pasa: algunas rarezas del legacy son bugs, y arreglarlas "de paso" se siente eficiente. Cómo detectarlo: al terminar la migración, el sistema no se comporta igual que antes en algunos casos, y nadie decidió eso explícitamente —se coló dentro del "migré"—. Cómo corregirlo: separa las dos cosas. Primero migra a paridad exacta con el legacy, bug incluido, para que la migración sea puramente estructural (misma salida, mejor código). Después, en un cambio aparte y deliberado, corrige el bug con su propia validación y su propio registro. Así, si algo se rompe, sabes si fue la migración o la corrección —no una mezcla ambigua de las dos—. El parallel-run te muestra la discrepancia; la disciplina es no resolverla cambiando comportamiento a escondidas.
Ejercicios
Ejercicio 1 — La panadería que hornea en paralelo. En la analogía, el panadero hornea la receta vieja y la nueva en paralelo durante una semana, comparándolas, pero vende siempre el pan viejo. (a) ¿Qué corresponde, en el parallel-run del código, a "vender siempre el pan viejo"? (b) ¿Qué es la "anotación al margen de la abuela" que la receta nueva se saltó? (c) ¿Por qué comparar los panes en una cata interna es mejor que enterarse por la queja de un cliente?
Ver solución
(a) Corresponde a la regla de oro: el parallel-run devuelve siempre el valor del legacy al llamador. El modern se calcula y se compara (se hornea y se cata), pero su resultado no llega al usuario (no se vende). Vender siempre el pan viejo = devolver siempre el legacy mientras la nueva no esté validada.
(b) Es una regla tácita del legacy que la reimplementación olvidó: en el ejemplo, "el envío gratis no aplica a international". Como la anotación al margen ("un reposo extra cuando hace calor"), es un detalle no obvio del comportamiento viejo que la versión nueva, más limpia, se saltó sin darse cuenta. El parallel-run la delata al comparar contra el legacy.
(c) Porque la cata interna atrapa el defecto antes de que llegue a un cliente: el panadero prueba el pan nuevo él mismo, en la sombra, y si sale plano lo detecta sin que ningún cliente reciba un pan malo. En el código, el parallel-run atrapa la discrepancia (international cobrando 0.0) en el reporte, no en un cliente que recibió envío gratis indebido. Enterarse por la queja de un cliente significa que el error ya causó daño; enterarse por la comparación en la sombra significa que lo atrapaste sin costo. La validación pasiva —correr y comparar sin exponer— es lo que compra esa seguridad.
Ejercicio 2 — Lee la discrepancia. El parallel-run con V1 reportó Discrepancias: 1 -> [(3, 'international', 29.0, 0.0)]. (a) Interpreta esa tupla campo por campo. (b) ¿Qué regla del legacy olvidó ModernShippingV1, y cómo lo sabes por el número? (c) Durante esta corrida, ¿qué valor recibió el "usuario" del pedido 3, y por qué eso importa?
Ver solución
(a) La tupla (3, 'international', 29.0, 0.0) dice: en el pedido 3, de zona international, el legacy calculó 29.0 y el modern (V1) calculó 0.0. Es un reporte accionable: identifica el caso exacto (pedido 3, international), y las dos cifras que difieren (29.0 vs 0.0), para que puedas ir directo a la regla que falla.
(b) Olvidó que el envío gratis no aplica a international. Lo sabes por el número: el pedido 3 tiene total 80.0, que supera el umbral de envío gratis (50.0). El legacy no lo hace gratis porque es international (cobra los 29.0 normales: base 25.0 + 4.0 de recargo). V1 sí lo hace gratis (0.0) porque su condición es solo total >= 50.0, sin excluir international. La diferencia de 29.0 a 0.0 es exactamente el costo de envío que V1 regaló por saltarse esa exclusión.
(c) Recibió 29.0 —el valor del legacy—, no el 0.0 del modern. Importa porque demuestra la regla de oro en acción: aunque V1 tenía el bug y calculó 0.0, ese valor nunca llegó al usuario; el parallel-run devolvió el legacy y solo reportó la diferencia. El cliente pagó lo correcto y el equipo se enteró del bug por el reporte, no por un incidente. Si el parallel-run hubiera devuelto el modern, ese cliente habría recibido envío gratis indebido —el bug habría llegado a producción—.
Ejercicio 3 — ¿Legacy o modern tiene razón? El parallel-run de un equipo reporta una discrepancia: para cierto pedido, el legacy cobra 12.0 y el modern cobra 10.0. Al investigar, descubren que el legacy suma un "recargo por manejo" de 2.0 que se agregó hace años y que el negocio hoy considera un error que debería eliminarse. (a) Según la regla del patrón, ¿qué implementación debe ganar durante la migración, y por qué? (b) ¿Qué harían con el recargo que el negocio quiere quitar? (c) ¿Por qué es importante no mezclar las dos cosas?
Ver solución
(a) Durante la migración debe ganar el legacy (cobrar 12.0): la regla del patrón es migrar a paridad primero. El objetivo de esta fase es que la implementación nueva se comporte igual que la vieja —incluido el recargo de 2.0, aunque parezca un error— para que la migración sea puramente estructural: mismo comportamiento observable, mejor código. ModernShipping debe arreglarse para incluir el recargo y coincidir con el legacy en 12.0.
(b) El recargo que el negocio quiere quitar se elimina en un cambio aparte y deliberado, después de que la migración esté completa y estable: su propia decisión, su propia validación, su propio registro (idealmente un ADR, que es tema de la guía de decisiones). No se resuelve dejando que el modern difiera "de paso" durante la migración.
(c) Porque mezclarlas hace imposible razonar sobre qué causó qué. Si el modern difiere del legacy y eso incluye tanto la migración como la eliminación del recargo, cuando algo se rompa no sabrás si fue el cambio de implementación o el cambio de comportamiento. Separándolas —migrar a paridad exacta primero, cambiar comportamiento después— cada paso tiene una sola causa y una sola validación. La migración prueba "misma salida, mejor código"; el cambio de recargo prueba "nueva regla de negocio, aprobada". Dos afirmaciones limpias en vez de una ambigua. Además, revertir es más simple: puedes deshacer el cambio de recargo sin tocar la migración, o al revés.
Resumen y siguiente paso
En esta lección hiciste la validación que autoriza el rollout: el parallel-run para probar que el cambio es seguro. Viste, con la panadería que hornea las dos recetas en paralelo y vende siempre el pan viejo, que puedes validar la implementación nueva contra la vieja sin exponer sus errores a nadie. Y lo ejecutaste con un bug real: una ModernShippingV1 que olvidó la regla tácita "international no es gratis", que el parallel-run atrapó en el pedido 3 con el número exacto de la diferencia (29.0 vs 0.0) —sin que ese 0.0 llegara a ningún usuario—. Arreglaste a ModernShippingV2, volviste a correr, y obtuviste Discrepancias: 0: la luz verde para subir el flag. Aprendiste la regla de oro (devolver siempre el legacy durante la prueba), qué demuestra un parallel-run (paridad en los casos corridos, no universal) y por qué las discrepancias se resuelven por defecto a favor del legacy, dejando las correcciones de comportamiento para un cambio aparte.
Antes de avanzar deberías poder: explicar qué es un parallel-run y su regla de oro; leer un reporte de discrepancia y traducirlo a la regla que falla; decir por qué cinco casos limpios no autorizan un salto al 100%; y distinguir "migrar a paridad" de "arreglar un bug del legacy", y por qué no se mezclan.
La lección 6 hace el último paso, el que casi nadie hace: borrar la implementación vieja. Cuando el flag llegó a 100% y el parallel-run se mantiene limpio, LegacyShipping ya no recibe tráfico ni aporta nada —solo pesa—. Vas a ejecutar el inventario de piezas móviles antes y después del borrado, y a medir el costo de dejar las dos "por si acaso": un requisito nuevo que obliga a editar cada implementación viva, y el drift real (un KeyError en la corrida) cuando se olvida una. Dejar las dos para siempre es la migración eterna, el fracaso silencioso del patrón; borrar la vieja es lo que paga la migración.
Recursos
- Martin Fowler, "Patterns of Legacy Displacement" — martinfowler.com/articles/patterns-legacy-displacement. Correr las dos implementaciones sobre la misma entrada y comparar sus resultados, con la vieja como fuente de verdad, para validar la nueva sin exponerla; el parallel run como patrón nombrado es de Sam Newman (Monolith to Microservices). La lectura directa de esta lección. En inglés.
- Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004) — los characterization tests como la forma de capturar el comportamiento del legacy (incluidas sus rarezas tácitas) que el parallel-run luego verifica que el modern reproduce. La conexión con el módulo 2 de esta guía. En inglés.
- GitHub, "Scientist" — github.com/github/scientist. La librería que popularizó el parallel-run en producción ("science experiments"): corre el código viejo y el nuevo en paralelo, devuelve el viejo, y reporta las discrepancias. La implementación real de lo que esta lección simula. En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3, sección "Parallel Run" — el parallel-run como técnica de verificación durante la migración de un monolito, con la advertencia de compararlo sobre tráfico real suficiente. En inglés.