From n00b to ZeroCool / Profesionalización
Refactor del monstruo: cómo salvar un backend viejo sin apagar el negocio
Plan realista para refactorizar un backend legacy sin tumbar producción: métricas, tests, Strangler Fig, migraciones seguras y deploy con rollback.
Lo que vale la pena leer aquí
Abres los logs y es puro ruido. Abres el repo y hay una carpeta que se llama new2finalfinal/. Intentas levantar el servicio en tu laptop vieja y el ventilador suena como si fuera a despegar desde la Central de Autobuses.
Intro con gancho
Son las 7:58 pm. Ya estabas en modo “cierro la laptop” y entra el mensaje del jefe: “Oye, el endpoint de pagos está lento otra vez. ¿Lo checas en corto?”.
Abres los logs y es puro ruido. Abres el repo y hay una carpeta que se llama new2_final_final/. Intentas levantar el servicio en tu laptop vieja y el ventilador suena como si fuera a despegar desde la Central de Autobuses.
Ese backend no es “malo”. Es un backend que sobrevivió. Pivotes, deadlines inventadas, features “urgentes porque el cliente ya pagó”, un deploy de viernes sin rollback, y parches que se quedaron para siempre.
La buena: sí se puede salvar. La mala: casi nunca se salva con “un gran refactor” de tres meses encerrado. Se salva con estrategia, medición y cambios chicos que no tiren producción.
Qué te vas a llevar
- Cómo decidir qué refactor sí paga cuando el sistema ya factura y está en production.
- Cómo armar una red de seguridad: métricas, logs útiles y tests que peguen donde duele.
- Un workflow realista con Strangler Fig y migraciones sin drama.
- Cómo manejar deuda técnica sin humo: tradeoffs, riesgos y comunicación.
- Errores típicos de guardia y cómo salir vivo.
Contexto práctico: por qué “reescribirlo todo” casi siempre sale caro
La fantasía suena bonita: “Lo reescribimos en el framework de moda, lo partimos en microservices y listo”.
La realidad pega distinto:
- El backend viejo trae reglas de negocio enterradas en lugares raros:
ifimposibles, stored procedures, cronjobs olvidados. - El big rewrite suele morir por paridad funcional: siempre falta “ese caso rarito” que solo pasa con un banco, un cliente enterprise o una integración que nadie quiere tocar.
- En México/LatAm hay condimentos extra: integraciones bancarias con horarios raros, picos de quincena, soporte diciendo “prod no se toca”… hasta que urge.
Si tu backend está pagando nómina, la jugada ganadora casi siempre es refactor incremental con métricas. En vez de “cambiar todo”, cambias lo suficiente para:
- bajar incidentes,
- hacer deploy sin rezar,
- mejorar performance donde importa,
- y permitir que el equipo siga sacando features.
Decisión con filo: el objetivo no es “código bonito”. Es menos riesgo y más velocidad sostenible.
Guía principal: refactor del monstruo en 7 pasos (sin apagar producción)
1) Ponle nombre al dolor (y mide antes de tocar)
Antes de refactorizar, ubica qué está rompiendo el negocio o reventando guardias. Dos preguntas que sí sirven:
- ¿Qué parte causa más incidentes?
- ¿Qué parte bloquea más el delivery porque nadie se atreve a tocarla?
Métricas mínimas (agarra las que puedas instrumentar ya):
- Latencia p95/p99 por endpoint.
- Tasa de errores 4xx/5xx.
- Timeouts a dependencias (DB, Redis, servicios externos).
- Saturación: CPU, memoria, pool de conexiones.
- Tiempos de jobs/colas.
Si ya tienes Prometheus/Grafana, chido. Si no, no te detengas: con logs estructurados y un dashboard básico te alcanza para empezar.
Ejemplo rápido (Node/Express) para logs con requestId:
import { randomUUID } from "crypto";
export function requestId(req, res, next) {
const id = req.headers["x-request-id"] ?? randomUUID();
req.requestId = id;
res.setHeader("x-request-id", id);
next();
}
export function logRequest(req, res, next) {
const start = Date.now();
res.on("finish", () => {
console.log(JSON.stringify({
level: "info",
requestId: req.requestId,
method: req.method,
path: req.originalUrl,
status: res.statusCode,
durationMs: Date.now() - start
}));
});
next();
}
Advertencia real: si no puedes medir impacto, no sabes si el refactor ayudó o solo “se siente mejor”.
2) Haz inventario: mapa de dependencias y “zonas radioactivas”
No necesitas un PDF de 40 páginas. Necesitas un mapa que quepa en una pantalla:
- Entradas: API, webhooks, cronjobs, colas.
- Salidas: DB, cache, servicios externos (pagos, correo, facturación).
- Datos: tablas críticas, flujos de escritura/lectura.
Marca tus “zonas radioactivas”:
- Código sin tests.
- Código con side effects (cobra, cancela, manda correos, mueve inventario).
- Lógica por cliente (“si
customer_idestá en… entonces”).
Tip de guerra: si alguien del equipo dice “esa parte no la toques”, ahí está el ROI… y el riesgo.
Decisión práctica: prioriza por riesgo + frecuencia de cambio. Lo que se toca mucho y rompe fácil se paga solo.
3) Crea una red de seguridad: tests donde sí duelen
No intentes arrancar buscando 90% de coverage. Es trampa.
Empieza con:
- Smoke tests de endpoints que facturan (login, pagos, crear orden).
- Contract tests para integraciones externas (webhooks, API de pagos).
- Golden master para comportamientos legacy: congelas la salida actual y la comparas mientras refactorizas.
Ejemplo “golden master” (Python + pytest) cuando todavía no quieres entender toda la talacha:
import json
def test_legacy_invoice_payload(snapshot, client):
resp = client.post("/invoices", json={"orderId": "123"})
assert resp.status_code == 200
snapshot.assert_match(json.dumps(resp.json(), sort_keys=True, indent=2))
Decisión con filo: si el negocio no tolera fallas, tu primer refactor real es hacer que los cambios fallen de forma detectable (tests + alertas), no “limpiar código”.
4) Aplica Strangler Fig: lo nuevo rodea a lo viejo
Este patrón funciona porque no te obliga a reescribir todo. Construyes lo nuevo alrededor:
- Pones una capa (router, gateway, feature flag).
- Mandas un porcentaje de tráfico al módulo nuevo.
- Monitoreas.
- Subes el porcentaje.
Pseudocódigo con feature flag:
if featureFlag("newPricing"):
return newPricingService.calculate(cart)
else:
return legacyPricing.calculate(cart)
La clave es empezar por una rebanada: un endpoint o una función aislable. No el core entero.
Tradeoff honesto: vas a cargar dos caminos un rato. Cuesta. Pero cuesta menos que romper pagos en quincena y quedarte sin plan B.

5) Refactor por capas: primero lo que tumba, luego lo que solo se ve feo
Orden recomendado (porque baja incidentes):
- Observabilidad (logs, métricas, alertas).
- Control de errores (timeouts, retries, circuit breakers).
- Acceso a datos (queries, índices, pools, transacciones).
- Separación por dominios (módulos/paquetes por bounded context).
- Estilo/limpieza (nombres, estructura, lint, formatting).
Ejemplo de timeout + retry con backoff (Node, idea general):
async function withTimeout(promise, ms) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), ms);
try {
return await promise(ctrl.signal);
} finally {
clearTimeout(t);
}
}
async function retry(fn, { retries = 2, baseMs = 150 } = {}) {
let lastErr;
for (let i = 0; i <= retries; i++) {
try { return await fn(); }
catch (e) {
lastErr = e;
await new Promise(r => setTimeout(r, baseMs * (i + 1)));
}
}
throw lastErr;
}
Decisión práctica: si tu backend se cae porque un proveedor externo se tarda, refactorizar “arquitectura” sin timeouts es maquillaje.
6) Migraciones de base de datos sin trauma (y sin bloquear la chamba)
Si el monstruo vive en la DB, no empieces con “cambiamos el esquema y ya”. Hazlo como si fueras a fallar (porque alguna vez va a fallar).
Estrategia que suele salvarte:
- Expand: agregas columnas/tablas nuevas sin romper lo viejo.
- Backfill: rellenas datos en background (jobs por lotes).
- Dual-write (temporal): escribes en viejo y nuevo.
- Cutover: cambias lecturas al nuevo.
- Contract: borras lo viejo cuando ya estás seguro.
El dual-write duele porque duplica riesgo. Úsalo solo si no hay de otra y pon:
- idempotencia,
- logs,
- alarmas de divergencia.
SQL ejemplo: expand sin romper
ALTER TABLE orders ADD COLUMN total_v2 NUMERIC(12,2);
CREATE INDEX idx_orders_total_v2 ON orders(total_v2);
Backfill por lotes (pseudocódigo):
for each batch of orders where total_v2 is null:
compute total_v2
update orders set total_v2 = ...
sleep a bit
Escena común: lo programas “en la madrugada porque baja el tráfico” y se te olvida que tu e-commerce vende más en quincena… y tu madrugada es hora pico. Por eso el throttle y los lotes.
7) Deploy con paracaídas: canary, rollback y runbook
Refactor sin plan de rollback es jugarle al valiente en production.
Mínimos que sí hacen diferencia:
- Feature flags para apagar lo nuevo.
- Canary release (1–5% del tráfico) si tu stack lo permite.
- Runbook: pasos claros para diagnosticar y revertir.
- SLO/alertas ligadas a negocio: éxito de pagos, creación de órdenes, tiempo de login.
Plantilla rápida de runbook (cópiala y úsala):
Síntoma:
- Aumenta 5xx en /checkout
Verificar:
- Dashboard: latencia p95, errores por proveedor
- Logs por requestId
- Cola de jobs (si aplica)
Mitigación:
- Apagar flag newCheckout=true
- Aumentar timeout del proveedor (temporal)
Rollback:
- Revertir deploy a versión estable
- Validar métricas 10 min
Postmortem:
- Qué cambió, por qué no lo vimos, acción concreta
Decisión con filo: el runbook no es para tu “yo” descansado. Es para tu “yo” con sueño, con internet chafa en el cel, contestando desde un Oxxo porque se fue la luz.
Screenshots sugeridos
- Dashboard con p95/p99 por endpoint y tasa de 5xx.
- Ejemplo de logs estructurados con
requestIdydurationMs. - Panel de feature flags (LaunchDarkly/Unleash o config interna).
- Diagrama simple de Strangler Fig (módulo nuevo rodeando legacy).
- Migración “expand/backfill/cutover” en tu herramienta (Flyway/Liquibase/Prisma/Alembic).
Errores comunes + solución
Error 1: “Primero limpiamos el código y luego medimos”
Síntoma: semanas moviendo carpetas, cero mejoras percibidas.
Solución: instrumenta primero. Si el refactor no mueve una métrica o no reduce incidentes, es un hobby caro.
Error 2: Reescritura big bang
Síntoma: rama eterna, conflictos, el negocio sigue pidiendo features.
Solución: Strangler Fig + entregas pequeñas. Un endpoint a la vez. Una tabla a la vez.
Error 3: Tests que no protegen el negocio
Síntoma: 80% coverage y aún así se rompe checkout.
Solución: smoke/contract/golden master sobre puntos críticos. Menos tests “bonitos”, más tests “que te salvan la guardia”.
Error 4: Migraciones con locks pesados
Síntoma: la DB se pone lenta, se acumulan timeouts, soporte se prende.
Solución: expand/backfill, índices bien pensados, lotes pequeños y pruebas en staging con volumen parecido.
Error 5: Refactor sin ownership
Síntoma: nadie entiende la parte nueva, nadie la toca, y creas otro monstruo.
Solución: dueños por módulo, runbooks, y regla simple: lo nuevo nace con alertas y tests mínimos.

Checklist final
- Tengo métricas básicas (latencia, errores, dependencias) antes del primer cambio.
- Identifiqué 1–3 flujos críticos (login/pagos/órdenes) y les puse smoke tests.
- Elegí una “rebanada” para estrangular (un endpoint, un módulo, una tabla).
- Usé feature flags o routing para mover tráfico sin drama.
- Hice migraciones con expand/backfill/cutover (no big bang).
- Tengo runbook y rollback probado (sí: probado).
- Puedo demostrar impacto: menos incidentes, mejor p95/p99 o menos tiempo de entrega.
FAQ
1) ¿Cómo sé si conviene refactor o reescritura?
Si el sistema todavía acepta cambios sin que cada pull request sea ruleta rusa, el refactor incremental suele ganar. Reescritura solo cuando la base ya no da: dependencias imposibles de actualizar, costos operativos absurdos o límites técnicos de verdad.
2) ¿Cuánto tiempo debería durar un refactor “bien hecho”?
No hay fecha mágica. Lo sano es entregar en incrementos de 1–2 semanas con impacto medible. Si pasan 4 semanas sin valor, algo se desvió: scope, métricas o enfoque.
3) ¿Qué hago si no me dejan tocar producción ni meter tooling?
Arranca con lo que sí controlas: logs más útiles, timeouts, manejo de errores y smoke tests en staging. Luego usa eso como evidencia: “con esto detectamos X y evitamos Y”. Esa evidencia abre puertas.
4) ¿Qué patrón recomiendas si todo está acoplado a la DB?
Strangler Fig aplicado a datos: expand/backfill/cutover. Y encapsula queries detrás de un repositorio/DAO para que el resto del código deje de hablar SQL directo en 40 lugares.
5) ¿Cómo lo vendo al negocio sin sonar a “quiero limpiar código”?
Habla en idioma negocio: menos caídas, menos tickets, menos tiempo para entregar features, menos costo de guardias. Llévate números: incidentes/mes, horas perdidas, p95 en endpoints que facturan.
Siguiente episodio
Vamos a agarrar este monstruo y ponerle visibilidad de verdad: tracing distribuido, logs con contexto y alertas que no sean puro spam.
Porque refactor sin visibilidad es como manejar de noche sin faros.
Idea para cerrar bien este post: toma una sola práctica de aquí y conviértela en algo que tu equipo pueda aplicar hoy.
Cuando un artículo aterriza en decisiones reales, deja de ser contenido y se vuelve ventaja.


