Módulo 4: Branch by abstraction
Proyecto: moderniza el ShippingCalculator de Mercado
Descripción
Este es el capstone del módulo. Durante siete lecciones instalaste el patrón completo de branch by abstraction: insertar la capa de abstracción como un seam, construir la implementación nueva detrás de ella, conmutar con un feature flag, validar con un parallel-run, borrar la implementación vieja, y —la razón profunda— hacer todo en main en pasos chicos para evitar el merge hell de una rama larga. Ahora lo aplicas de punta a punta a un caso real: el cálculo de envío de Mercado, esa función legacy enredada que se llama desde media docena de lugares del monolito y que no tiene una frontera de red donde poner un proxy. Te toca modernizarla con branch by abstraction, ejecutado, y producir una bitácora de migración que un equipo pueda leer y reproducir.
El trabajo del proyecto es el trabajo real de modernizar código interno vivo: llevar el ShippingCalculator de "una función legacy que da miedo tocar" a "una implementación moderna, validada, con la vieja borrada" —sin apagar el sistema y sin una rama de larga vida—. Integra las seis fases del patrón en un solo programa ejecutado, como una bitácora de migración: cada fase deja una evidencia medida (transparencia verificada, discrepancias atrapadas y arregladas, burn-down del legacy a cero, piezas móviles reducidas, conflictos de merge en cero). Fíjate en la frontera, que es deliberada: este proyecto no extrae el envío a su propio servicio (eso es el módulo 5), ni migra datos a otro almacén (módulo 6), ni decide formalmente con un ADR si conviene migrar (esa es la guía de decisiones). Hace una cosa a fondo: cambiar la implementación interna del cálculo de envío, del legacy al modern, por dentro del código, de forma incremental y medida.
Conexión con el módulo. Es la integración de las siete lecciones en un solo entregable ejecutado. La Fase 1 usa la abstracción y la prueba de transparencia de la lección 2; la Fase 2, la construcción de ModernShipping de la lección 3 y el parallel-run de la lección 5 (que atrapa la discrepancia); la Fase 3, el arreglo a paridad de la lección 5; la Fase 4, el feature flag y el rollout de la lección 4, con el gate que solo promueve si el parallel-run está limpio; la Fase 5, el borrado del legacy de la lección 6; y la Fase 6, la medición de conflictos de la lección 7. Al terminarlo, tendrás el patrón completo aplicado a un caso, ejecutado y medido, con la justificación de por qué incremental en main venció a la rama larga.
La solución de referencia, ejecutada
Vamos a construir la solución en un solo programa que corre las seis fases sobre el ShippingCalculator de Mercado, dejando una bitácora. Todo con datos fijos —300 pedidos deterministas—, reproducible.
Parte 1 — Los datos y las piezas
Tenemos la abstracción ShippingCalculator, la implementación LegacyShipping (la lógica de envío de hoy), y dos versiones de la nueva: ModernShippingV1 (con un bug de paridad, olvida que el envío gratis no aplica a international) y ModernShippingV2 (arreglada). El tráfico son 300 pedidos deterministas que cubren las tres zonas, distintos pesos y totales. Una función _base_formula comparte la estructura del cálculo, parametrizada por la regla de envío gratis, para que las tres implementaciones sean diffables y la diferencia esté exactamente en esa regla.
import zlib
from typing import Protocol
# ======================================================================
# Abstraccion + implementaciones (la pieza que migramos: ShippingCalculator)
# ======================================================================
class ShippingCalculator(Protocol):
def cost(self, order: dict) -> float: ...
def _base_formula(order, free_rule):
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 free_rule(order):
cost = 0.0
return round(cost, 2)
class LegacyShipping:
def cost(self, order):
return _base_formula(order, lambda o: o["order_total"] >= 50.0 and o["zone"] != "international")
class ModernShippingV1: # con el bug: envio gratis tambien a international
def cost(self, order):
return _base_formula(order, lambda o: o["order_total"] >= 50.0)
class ModernShippingV2: # arreglada
def cost(self, order):
return _base_formula(order, lambda o: o["order_total"] >= 50.0 and o["zone"] != "international")
# ----- Trafico fijo (300 pedidos deterministas) -----
ZONES = ["local", "national", "international"]
ORDERS = [{
"id": i, "zone": ZONES[i % 3],
"weight_kg": round(0.5 + (i % 5) * 0.5, 1),
"order_total": 20.0 + (i % 7) * 10.0,
"items_subtotal": 20.0 + (i % 7) * 10.0,
} for i in range(1, 301)]
def bucket(oid): return zlib.crc32(str(oid).encode()) % 100
def checkout_total(order, calc): return round(order["items_subtotal"] + calc.cost(order), 2)
def parallel_run(orders, legacy, modern):
return [(o["id"], o["zone"], legacy.cost(o), modern.cost(o))
for o in orders if legacy.cost(o) != modern.cost(o)]
Parte 2 — El programa completo: las seis fases
Las seis fases, encadenadas, cada una dejando su evidencia en una bitácora:
ledger = []
legacy = LegacyShipping()
# ======================================================================
# FASE 1 - Insertar la abstraccion. Transparencia: los llamadores no cambian.
# ======================================================================
def inline_legacy(order): # la formula "pre-abstraccion", suelta en el monolito
return _base_formula(order, lambda o: o["order_total"] >= 50.0 and o["zone"] != "international")
transparent = all(inline_legacy(o) == legacy.cost(o) for o in ORDERS)
print(f"FASE 1 insertar abstraccion -> transparente en {len(ORDERS)} pedidos: {transparent}")
ledger.append(("1 abstraccion", f"transparencia {transparent}"))
# ======================================================================
# FASE 2 - Construir modern al lado. parallel-run v1 delata una discrepancia.
# ======================================================================
mm_v1 = parallel_run(ORDERS, legacy, ModernShippingV1())
print(f"FASE 2 parallel-run modern v1 -> discrepancias: {len(mm_v1)} ej: {mm_v1[0] if mm_v1 else '-'}")
ledger.append(("2 parallel-run v1", f"{len(mm_v1)} discrepancias -> NO conmutar"))
# ======================================================================
# FASE 3 - Arreglar (v2). parallel-run limpio: seguro conmutar el flag.
# ======================================================================
mm_v2 = parallel_run(ORDERS, legacy, ModernShippingV2())
print(f"FASE 3 parallel-run modern v2 -> discrepancias: {len(mm_v2)} (seguro subir el flag)")
ledger.append(("3 parallel-run v2", f"{len(mm_v2)} discrepancias -> conmutar OK"))
# ======================================================================
# FASE 4 - Ramp del flag 0 -> 25 -> 50 -> 100 con gate y burn-down del legacy.
# ======================================================================
modern = ModernShippingV2()
def resolve(order, pct): return modern if bucket(order["id"]) < pct else legacy
print("\nFASE 4 ramp del flag (gate: solo sube si parallel-run limpio)")
print(f" {'rollout':>8}{'-> modern':>11}{'-> legacy':>11}{'callers OK':>12}{'gate':>8}")
for pct in (0, 25, 50, 100):
counts = {"modern": 0, "legacy": 0}
callers_ok = True
for o in ORDERS:
calc = resolve(o, pct)
counts["modern" if calc is modern else "legacy"] += 1
callers_ok = callers_ok and (checkout_total(o, calc) == checkout_total(o, legacy))
gate = "PROMUEVE" if len(mm_v2) == 0 else "BLOQUEA"
print(f" {pct:>7}%{counts['modern']:>11}{counts['legacy']:>11}{str(callers_ok):>12}{gate:>10}")
ledger.append(("4 ramp 0->100", "burn-down legacy a 0, callers OK en todo nivel"))
# ======================================================================
# FASE 5 - Borrar el legacy. Piezas moviles: 4 -> 2.
# ======================================================================
before_parts, after_parts = 4, 2 # legacy+modern+flag+wiring -> modern+wiring
print(f"\nFASE 5 borrar el legacy -> piezas moviles: {before_parts} -> {after_parts} (flag y legacy fuera)")
ledger.append(("5 borrar legacy", f"piezas {before_parts}->{after_parts}, una sola fuente de verdad"))
# ======================================================================
# FASE 6 - Todo esto ocurrio en main, en pasos chicos: 0 conflictos de merge.
# ======================================================================
main_weekly = [{10, 11, 12}, {12, 20, 21}, {5, 6, 30}, {21, 22, 40}]
long_branch = set(range(5, 25))
running = set().union(*main_weekly)
conflicts_long = len(running & long_branch)
print(f"FASE 6 integracion -> rama larga: {conflicts_long} conflictos | branch-by-abstraction: 0")
ledger.append(("6 integracion", f"rama larga {conflicts_long} conflictos vs 0 en main"))
# ======================================================================
# Bitacora de migracion
# ======================================================================
print("\n=== bitacora de migracion (ShippingCalculator, legacy -> modern) ===")
for phase, evidence in ledger:
print(f" [{phase:<18}] {evidence}")
print("\n Incremental gano: cada paso, medido y siempre integrable en main.")
Qué esperar. Al correr el archivo completo, la salida es exactamente esta:
FASE 1 insertar abstraccion -> transparente en 300 pedidos: True
FASE 2 parallel-run modern v1 -> discrepancias: 57 ej: (5, 'international', 25.0, 0.0)
FASE 3 parallel-run modern v2 -> discrepancias: 0 (seguro subir el flag)
FASE 4 ramp del flag (gate: solo sube si parallel-run limpio)
rollout -> modern -> legacy callers OK gate
0% 0 300 True PROMUEVE
25% 77 223 True PROMUEVE
50% 156 144 True PROMUEVE
100% 300 0 True PROMUEVE
FASE 5 borrar el legacy -> piezas moviles: 4 -> 2 (flag y legacy fuera)
FASE 6 integracion -> rama larga: 8 conflictos | branch-by-abstraction: 0
=== bitacora de migracion (ShippingCalculator, legacy -> modern) ===
[1 abstraccion ] transparencia True
[2 parallel-run v1 ] 57 discrepancias -> NO conmutar
[3 parallel-run v2 ] 0 discrepancias -> conmutar OK
[4 ramp 0->100 ] burn-down legacy a 0, callers OK en todo nivel
[5 borrar legacy ] piezas 4->2, una sola fuente de verdad
[6 integracion ] rama larga 8 conflictos vs 0 en main
Incremental gano: cada paso, medido y siempre integrable en main.
Parte 3 — La bitácora, leída fase por fase
La Fase 1 insertó la abstracción y verificó la transparencia: transparente en 300 pedidos: True. Se comparó, para los 300 pedidos, la fórmula "pre-abstracción" (la función suelta que vivía en el monolito) contra LegacyShipping.cost a través de la abstracción, y todos coincidieron. Es el paso de menor riesgo: cambiaste la estructura del código —ahora todo pasa por ShippingCalculator— sin cambiar un solo resultado. El seam quedó instalado, con LegacyShipping del otro lado, listo para conmutar.
La Fase 2 construyó ModernShippingV1 y la validó con parallel-run —y el parallel-run la reprobó—: discrepancias: 57, con el ejemplo (5, 'international', 25.0, 0.0). Fíjate en el número: 57 de los 300 pedidos difieren. No es una casualidad ni un caso raro: es toda la porción de pedidos international con total suficiente para gatillar el (mal) envío gratis de V1. El ejemplo lo muestra —pedido 5, international, el legacy cobra 25.0 y V1 cobra 0.0—: V1 olvidó que el envío gratis no aplica a international, y el parallel-run lo atrapó en 57 pedidos concretos. La evidencia en la bitácora es tajante: 57 discrepancias -> NO conmutar. Con una sola discrepancia bastaría para no subir el flag; con 57, es evidente que V1 no está lista.
La Fase 3 arregló el bug (ModernShippingV2, que agrega el and zone != international) y volvió a correr el parallel-run: discrepancias: 0. Las 57 diferencias desaparecieron —V2 replica la regla tácita del legacy—. La bitácora registra 0 discrepancias -> conmutar OK: ahora, y solo ahora, es seguro subir el flag. El contraste entre la Fase 2 (57) y la Fase 3 (0) es el valor del parallel-run en una línea: atrapó un bug que habría cobrado envío gratis indebido a 57 grupos de pedidos, antes de exponerlo a un solo usuario.
La Fase 4 subió el flag del 0 al 100% con un gate. Lee la tabla de arriba abajo: a 0% los 300 pedidos van al legacy; a 25%, 77 al modern y 223 al legacy; a 50%, 156 y 144; a 100%, los 300 al modern y 0 al legacy —el burn-down del legacy llegó a cero—. La columna callers OK da True en todos los niveles: conmutar la implementación no rompió a los llamadores en ningún punto del rollout. Y la columna gate dice PROMUEVE en cada nivel porque el parallel-run de la Fase 3 quedó limpio (0 discrepancias); si hubiera quedado con discrepancias, el gate diría BLOQUEA y el rollout no avanzaría. Esa es la disciplina: el flag sube sobre lo validado, no a ciegas.
La Fase 5 borró el legacy: piezas moviles: 4 -> 2. Con el flag en 100% y el parallel-run limpio, LegacyShipping ya no recibía tráfico ni aportaba nada. Borrarlo —junto con el flag y el wiring de selección— bajó las piezas móviles de cuatro (legacy, modern, flag, wiring) a dos (modern, wiring trivial). La bitácora lo resume: una sola fuente de verdad. Terminó el doble mantenimiento; el drift se volvió imposible.
La Fase 6 cerró el arco con la medición que da nombre al patrón: rama larga: 8 conflictos | branch-by-abstraction: 0. Todo lo anterior —insertar, construir, validar, conmutar, borrar— ocurrió en main, en pasos chicos, cada uno integrable de inmediato. Si el mismo refactor se hubiera hecho en una rama de larga vida, habría acumulado 8 líneas en conflicto al mergear (la divergencia de 4 semanas sin integrar); hecho con branch by abstraction en main, tuvo 0. La bitácora final lo dice entero, fase por fase, y cierra con la tesis del módulo: Incremental gano: cada paso, medido y siempre integrable en main.
Este proyecto es el patrón completo aplicado a un caso, y encaja así en el arco de la guía:
flowchart LR
P["Proyecto M4<br/>ShippingCalculator: legacy -> modern<br/>(migracion interna, por codigo)"] --> M5["M5<br/>extraer un servicio<br/>(anti-corruption layer)"]
M5 --> M6["M6<br/>migrar los datos"]
M6 --> M7["M7<br/>medir el progreso"]
Léelo así: aquí modernizaste la implementación del cálculo de envío por dentro del monolito, sin sacarla a ningún lado. El módulo 5 enseña el paso siguiente —extraer una pieza a su propio servicio con un anti-corruption layer—, para cuando el objetivo no es solo cambiar la implementación sino mover la pieza fuera del monolito.
Tu entrega
Reproduce y adapta la solución de referencia. Tu entrega tiene tres piezas:
- La migración ejecutada de las seis fases: el programa que corre las seis fases sobre el
ShippingCalculator, con la salida literal. Puedes usar las implementaciones del ejemplo o —mejor— agregar una regla propia al cálculo de envío (por ejemplo, un descuento por volumen o una zona nueva) y asegurarte de que elModernShippingla replica, verificado por el parallel-run. - La bitácora de migración: la lista de fases con su evidencia medida (transparencia, discrepancias atrapadas y arregladas, burn-down, piezas móviles, conflictos). Si cambiaste las reglas, tus números serán distintos; lo que no cambia es la estructura: cada fase deja una evidencia, no una afirmación.
- La justificación: un párrafo que explique, con los números de tu corrida, por qué branch by abstraction en
mainvenció a la alternativa de la rama larga —usando el contraste de conflictos (los tuyos vs 0) y el hecho de que cada paso fue integrable de inmediato—. No extraigas el servicio ni migres datos; justifica por qué la migración interna fue incremental, medida y sin merge hell.
Errores comunes
Saltarse el parallel-run y subir el flag "porque el modern se ve bien". Qué pasa: el proyecto construye ModernShipping, lo prueba a ojo con un par de casos, y salta directo al rollout del flag sin correr el parallel-run sobre todos los pedidos. Por qué pasa: el parallel-run se siente redundante ("ya revisé, está bien"). Cómo detectarlo: si tu bitácora no tiene una fase de discrepancias medidas antes del rollout, te la saltaste. La solución de referencia lo hace evidente: ModernShippingV1 "se veía bien" y tenía 57 discrepancias. Cómo corregirlo: el parallel-run va antes del flag, sobre todo el tráfico (o una muestra grande), y el gate del rollout solo promueve si dio 0 discrepancias. La Fase 2 existe precisamente para atrapar el bug que "a ojo" no se ve. Sin esa fase, subes el flag con un modern que difiere del legacy en decenas de casos, y esos casos son usuarios reales cobrando mal.
Terminar en la Fase 4 (flag al 100%) y no borrar el legacy. Qué pasa: el proyecto llega al flag en 100%, ve callers OK: True, y se declara terminado —dejando LegacyShipping y el flag en el código—. Por qué pasa: llegar al 100% se siente como el final, y borrar código da respeto. Cómo detectarlo: tu bitácora tiene cinco fases, no seis; falta la reducción de piezas móviles. Cómo corregirlo: la migración termina en la Fase 5, con el legacy borrado y las piezas móviles de 4 a 2. El flag al 100% es "lo nuevo ya sirve"; borrar el legacy es "la migración terminó". Sin ese paso, quedas con dos implementaciones, doble mantenimiento y drift latente —la migración eterna—. La bitácora completa tiene seis fases, y la quinta es la que paga.
Hacer todo el proyecto en una rama larga y mergear al final. Qué pasa: el proyecto usa la mecánica del patrón —abstracción, dos implementaciones, flag— pero hace todo el trabajo en una rama que no se integra hasta el final, contradiciendo la Fase 6. Por qué pasa: la costumbre de "trabajo en mi rama y mergeo cuando esté completo" sobrevive a la teoría. Cómo detectarlo: si tu justificación dice "0 conflictos porque integré en main en pasos chicos" pero en la práctica hiciste todo en una rama y mergeaste una vez, tu bitácora miente. Cómo corregirlo: el punto de la Fase 6 es que cada fase es un commit (o varios) integrable en main de inmediato —la abstracción hoy, ModernShipping apagada mañana, el flag al 25% pasado mañana—. La justificación de "incremental venció a la rama larga" solo es honesta si de verdad integraste cada paso. Encerrar el patrón en una rama larga te da la divergencia de la rama y la complejidad del patrón: lo peor de ambos.
Ejercicios
Ejercicio 1 — Agrega una regla y revalida. Mercado quiere un descuento de envío: para pedidos con 3 o más artículos (item_count >= 3), el envío tiene un 50% de descuento (salvo que ya sea gratis). (a) Agrega la regla a ModernShipping y decide si también va en el legacy para esta migración. (b) ¿Qué mostraría el parallel-run si agregas la regla solo al modern? (c) ¿Cómo deberías introducir esta regla si es un cambio de comportamiento nuevo y no parte de la migración a paridad?
Ver solución
(a) Depende de qué sea la regla. Si el descuento por volumen ya existía en el legacy (era comportamiento actual que la reimplementación debe preservar), va en las dos para esta migración —la meta es paridad—. Si es un requisito nuevo que el negocio quiere estrenar, no va en esta migración: la migración es a paridad con el legacy, y meter una regla nueva la contamina. La pregunta clave es: ¿el legacy ya hacía esto? Si sí, replícalo en el modern para coincidir; si no, es un cambio aparte.
(b) Si el descuento no existía en el legacy y lo agregas solo al modern, el parallel-run mostraría discrepancias en todos los pedidos con item_count >= 3: el legacy cobra el envío completo y el modern cobra la mitad. El parallel-run no puede distinguir "bug" de "cambio de comportamiento deliberado" —solo ve que difieren—, así que reportaría esas diferencias como discrepancias, bloqueando el gate. Eso es correcto: te obliga a decidir explícitamente si esa diferencia es intencional.
(c) Como un cambio aparte, después de la migración. Primero completas branch by abstraction a paridad exacta (parallel-run en 0), borras el legacy, y quedas con ModernShipping como única fuente de verdad. Después, en un cambio deliberado y con su propia validación, agregas el descuento por volumen —idealmente detrás de su propio flag para poder subirlo gradual—. Así separas "migré la implementación" (misma salida, mejor código) de "cambié la regla de negocio" (nueva salida, aprobada). Si mezclaras las dos, el parallel-run no podría validar la migración (siempre habría "discrepancias" que en realidad son la regla nueva), y no sabrías si un problema vino del cambio de implementación o del de comportamiento.
Ejercicio 2 — Lee la bitácora. La bitácora final registró [2 parallel-run v1] 57 discrepancias -> NO conmutar y [3 parallel-run v2] 0 discrepancias -> conmutar OK. (a) ¿Por qué exactamente 57 de 300 pedidos discreparon con V1? (b) ¿Qué habría pasado si el gate de la Fase 4 hubiera corrido con V1 en vez de V2? (c) ¿Por qué la bitácora registra evidencia medida en cada fase en vez de solo "hecho / no hecho"?
Ver solución
(a) Porque 57 es la cantidad de pedidos international que superan el umbral de envío gratis (total >= 50.0) en el lote de 300. ModernShippingV1 los hace gratis (0.0) porque olvidó excluir international; el legacy los cobra normal. Todos esos pedidos —y solo esos— difieren. No es un número arbitrario: es exactamente la porción de tráfico que el bug de V1 afecta, medida. (Los pedidos international que no superan 50.0, o los de otras zonas, coinciden, por eso no son los 100 pedidos international completos.)
(b) El gate habría dicho BLOQUEA en vez de PROMUEVE, y el rollout no habría avanzado del 0%. El gate está condicionado a len(mm_v2) == 0; si el parallel-run vigente tuviera 57 discrepancias, la condición sería falsa y el flag no subiría. Es la protección funcionando: el gate no deja exponer a usuarios una implementación que difiere del legacy en 57 casos. Solo tras arreglar a V2 (0 discrepancias) el gate promueve.
(c) Porque "hecho / no hecho" no prueba nada —un equipo puede decir que validó sin haberlo hecho—. La evidencia medida (transparencia True en 300, 57 discrepancias, 0 discrepancias, burn-down a 0, piezas 4→2, 8 conflictos vs 0) es verificable y reproducible: cualquiera puede correr el código y obtener los mismos números. Una bitácora de evidencia convierte la migración de "confía en que lo hicimos bien" a "aquí están las mediciones de cada paso". Es la diferencia entre afirmar y demostrar —el principio de toda la guía: nada se afirma de memoria, todo se ejecuta—.
Ejercicio 3 — Cierra el arco: de la implementación al servicio. Este proyecto modernizó la implementación del cálculo de envío por dentro del monolito. El siguiente paso posible es extraerlo a su propio servicio (módulo 5). (a) ¿Qué diferencia hay entre lo que hiciste aquí y "extraer el servicio de envío"? (b) ¿Por qué tiene sentido hacer branch by abstraction antes de extraer? (c) ¿Qué pieza nueva aparecería al extraer que no existió en esta migración interna?
Ver solución
(a) Aquí cambiaste la implementación interna del cálculo de envío (de LegacyShipping a ModernShipping) sin sacarla del monolito: sigue siendo código que corre en el mismo proceso, llamado como una función. Extraer el servicio (módulo 5) es mover esa pieza a su propio proceso —un servicio aparte, con su propio despliegue y quizás su propia base de datos—, de modo que el monolito le hable por la red en vez de por una llamada de función. Uno cambia el cómo se calcula; el otro cambia el dónde vive y cómo se le habla.
(b) Porque branch by abstraction deja el cálculo de envío detrás de una abstracción limpia (ShippingCalculator) y en una implementación moderna y validada (ModernShipping). Extraer a un servicio es mucho más fácil desde ahí: ya tienes una frontera clara (la interfaz) y una implementación ordenada que mover, en vez de una función legacy enredada que primero habría que desenmarañar. Modernizar por dentro primero, extraer después, es el orden que reduce el riesgo —cada paso parte de un estado más limpio que el anterior—.
(c) Aparecería el anti-corruption layer: una capa que traduce entre el modelo del monolito y el modelo del servicio de envío extraído, para que el monolito pueda seguir hablando en sus términos viejos mientras el servicio nuevo usa los suyos, sin que las rarezas de uno contaminen al otro. En esta migración interna no hizo falta —todo vivía en el mismo modelo y el mismo proceso—; al cruzar la frontera de proceso, la traducción entre modelos se vuelve necesaria. Es la pieza central del módulo 5.
Resumen y siguiente paso
Cerraste el módulo aplicando branch by abstraction completo a un caso real. Tomaste el cálculo de envío de Mercado —una función legacy interna, sin frontera de red— y lo modernizaste de punta a punta, ejecutado y medido, en una bitácora de seis fases: insertaste la abstracción con transparencia verificada en 300 pedidos; construiste ModernShipping y el parallel-run atrapó 57 discrepancias de la v1 (el bug de international); las arreglaste en la v2 y el parallel-run quedó en 0; subiste el flag del 0 al 100% con un gate que solo promueve validado y el burn-down del legacy a cero, sin romper a los llamadores; borraste el legacy y bajaste las piezas móviles de 4 a 2; y demostraste que, al vivir todo en main en pasos chicos, el refactor tuvo 0 conflictos de merge contra los 8 de una rama larga. No extrajiste el servicio ni migraste datos: hiciste la migración interna de la implementación, incremental, medida y sin merge hell.
Con esto termina el módulo 4. Ya tienes las dos técnicas de migración incremental que se complementan: el strangler fig (módulo 3) para cuando hay una frontera de red donde poner un proxy externo, y branch by abstraction (este módulo) para cuando la migración es por dentro del código, sin esa frontera. Sabes insertar la abstracción como seam, construir la nueva detrás, conmutar con un flag, validar con parallel-run, borrar la vieja, y por qué todo eso en main evita el merge hell.
El módulo 5 da el paso siguiente: extraer un servicio. Hasta ahora modernizaste dentro del monolito —desviando tráfico (M3) o cambiando la implementación por dentro (M4)—. El módulo 5 saca una pieza fuera: extraer un bounded context (como el catálogo) a su propio servicio, con el anti-corruption layer que traduce entre el modelo viejo y el nuevo, la propiedad de los datos, y la base de datos compartida transitoria y cómo se corta. La abstracción limpia que dejaste en este módulo es, muchas veces, el punto de partida de esa extracción.
Recursos
- Martin Fowler, "BranchByAbstraction" (2014) — martinfowler.com/bliki/BranchByAbstraction.html. El patrón completo que este proyecto ejecuta de punta a punta: abstracción, dos implementaciones, conmutación y remoción de la vieja, todo en el mainline. En inglés.
- Paul Hammant, "branchbyabstraction.com" — branchbyabstraction.com. El paso a paso del patrón por el autor que más lo ha documentado, con casos reales de aplicación incremental en
main. En inglés. - Jez Humble y David Farley, Continuous Delivery (Addison-Wesley, 2010) — el marco de integración continua y trunk-based development donde branch by abstraction es la técnica para los cambios grandes; la justificación medida de por qué incremental en
mainvence a la rama larga. En inglés. - Martin Fowler, "Patterns of Legacy Displacement" — martinfowler.com/articles/patterns-legacy-displacement. La técnica de validación que atrapó las 57 discrepancias de la v1 antes de exponerlas: correr las dos implementaciones y comparar, con la vieja como fuente de verdad; el parallel run como patrón nombrado es de Sam Newman (Monolith to Microservices). En inglés.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), cap. 3 — hacia dónde sigue este trabajo: extraer la pieza modernizada a su propio servicio con un anti-corruption layer, el módulo 5 de esta guía. En inglés.