Transformar un monolito en una arquitectura de microservicios requiere más que dividir código. Es un proceso de descubrimiento del dominio, ajuste organizacional y gestión del cambio tecnológico. En esta sección exploramos las estrategias que permiten migrar gradualmente, reducir riesgos y obtener valor tempranamente sin detener la operación.
Cada migración comienza con una radiografía del monolito para comprender sus dependencias, límites funcionales y puntos críticos. El objetivo es identificar módulos con alta cohesión que puedan transformarse en servicios independientes y reconocer las dependencias compartidas que dificultan la separación.
El análisis incluye revisar diagramas de arquitectura, métricas de uso, historia de cambios y nivel de deuda técnica. También se examinan los flujos de datos y los contratos externos. Herramientas de arquitectura inversa, como la generación automática de grafos de dependencias, aportan información objetiva.
Un inventario estructurado ayuda a priorizar la migración. A continuación se muestra un ejemplo sencillo en formato de tabla con información relevante para la toma de decisiones.
record Capability(String nombre,
Importance importancia,
TechnicalRisk riesgo,
Set<String> dependencias) {}
List<Capability> capacidades = List.of(
new Capability("Facturación", Importance.ALTA, TechnicalRisk.MEDIO, Set.of("Pagos", "Clientes")),
new Capability("Catálogo", Importance.MEDIA, TechnicalRisk.BAJO, Set.of("Inventario")),
new Capability("Reportes", Importance.BAJA, TechnicalRisk.ALTO, Set.of("Facturación", "Pagos"))
);
Con esta información se evalúa qué capacidades generan mayor valor si se independizan y cuáles deben permanecer en el monolito hasta reducir su complejidad.
El Strangler Pattern propone rodear el monolito con una capa de integración que dirige parte del tráfico hacia nuevos servicios. A medida que las funcionalidades son reescritas, la capa desvía las peticiones al microservicio correspondiente y reduce la responsabilidad del sistema original.
La estrategia evita interrupciones masivas porque el sistema sigue operativo durante la migración. También permite revertir rápidamente si surge un problema, simplemente redirigiendo el tráfico al monolito.
El siguiente fragmento redirige peticiones según la versión disponible. Mientras una funcionalidad se migra, el gateway evalúa si debe utilizar el microservicio nuevo o continuar con el monolito.
@Bean
public RouteLocator migrationRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("catalogo_v2", r -> r
.path("/api/catalog/**")
.and()
.header("X-Migration-Flag", "catalog-v2")
.uri("lb://catalog-service"))
.route("catalogo_legacy", r -> r
.path("/api/catalog/**")
.uri("http://monolith.internal"))
.build();
}
El gateway permite experimentar con grupos de usuarios o entornos específicos antes de retirar definitivamente la funcionalidad legada.
Una táctica efectiva consiste en desarrollar las nuevas capacidades directamente como microservicios, evitando incrementar el monolito. Estos servicios conviven con el sistema existente y se integran mediante eventos o APIs. Así se construye experiencia operativa mientras el monolito permanece estable.
Cuando una nueva función requiere datos del monolito, se implementan adaptadores que exponen contratos específicos sin abrir la base de datos. Esto reduce acoplamientos y facilita la migración posterior.
El siguiente código ejemplifica cómo el monolito puede emitir eventos para que un microservicio de recomendaciones consuma cambios relevantes sin acoplarse a la base original.
public class MonolithEventPublisher {
private final KafkaTemplate<String, RecommendationEvent> kafkaTemplate;
public MonolithEventPublisher(KafkaTemplate<String, RecommendationEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publishProductView(String userId, String sku) {
RecommendationEvent event = new RecommendationEvent(userId, sku, Instant.now());
kafkaTemplate.send("product-views", userId, event);
}
}
El microservicio receptor mantiene su propia base de datos y puede escalar sin afectar el rendimiento del monolito.
La deuda técnica acumulada en el monolito determina la complejidad de la migración. Código sin pruebas, dependencias obsoletas o falta de documentación incrementan el costo de separación. Es esencial catalogar la deuda y priorizar el saneamiento según el impacto en la migración.
La decisión de migrar un módulo debe basarse en criterios objetivos: valor de negocio, frecuencia de cambios, nivel de riesgo, dependencia de sistemas externos y disponibilidad de equipo experto. No todo componente merece ser un microservicio; algunos módulos pueden permanecer en un componente modularizado mientras el resto evoluciona.
El siguiente evaluador asigna un puntaje compuesto para fundamentar la priorización.
record MigrationCandidate(String modulo,
int valorNegocio,
int costoOperativo,
int deudaTecnica,
int dependenciaExterna) {
int score() {
return valorNegocio * 3 + costoOperativo * 2 + deudaTecnica + dependenciaExterna;
}
}
List<MigrationCandidate> candidatos = fetchCandidates();
List<MigrationCandidate> ordenados = candidatos.stream()
.sorted(Comparator.comparing(MigrationCandidate::score).reversed())
.toList();
La puntuación orienta discusiones entre negocio y tecnología, estableciendo un backlog de migración realista y alineado con la estrategia corporativa.