Empezamos la octava entrega de esta serie de post de patrones de arquitectura donde hablaremos de patrones utilizados en las migraciones de software.

Aquí os dejo los anteriores posts de la serie, por si te has perdido alguno.

  1. Patrones de arquitectura de microservicios, ¿qué son y qué ventajas nos ofrecen?
  2. Patrones de arquitectura: organización y estructura de microservicios.
  3. Patrones de arquitectura: comunicación y coordinación de microservicios.
  4. Patrones de arquitectura de microservicios: SAGA, API Gateway y Service Discovery.
  5. Patrones de arquitectura de microservicios: Event Sourcing y arquitectura orientada a eventos (EDA).
  6. Patrones de arquitectura de microservicios: comunicación y coordinación con CQRS, BFF y Outbox.
  7. Patrones de microservicios: escalabilidad y gestión de recursos con escalado automático.

Hoy nos centraremos en 3 patrones de migración.

Introducción a los patrones de migración en microservicios

La migración de aplicaciones monolíticas a arquitecturas basadas en microservicios ha ganado popularidad en la última década, especialmente en industrias que buscan escalar de forma más eficiente y mejorar la velocidad de desarrollo. Las arquitecturas monolíticas, aunque robustas, presentan desafíos como la dificultad para escalar independientemente diferentes componentes, la complejidad del mantenimiento e integración continua y la falta de flexibilidad en equipos grandes. En contraste, los microservicios permiten un desarrollo más ágil, escalabilidad independiente y facilitan el mantenimiento y despliegue continuo de nuevas funcionalidades.

Sin embargo, la migración de un sistema monolítico a microservicios es una tarea compleja que, si no se maneja con cuidado, puede generar una serie de problemas. Aquí es donde los patrones de migración proporcionan un enfoque sistemático para hacer esta transición más suave y controlada. A continuación, detallamos tres patrones clave para esta migración: strangler pattern, patrón anticorrupción y descomposición por dominio.

Strangler pattern (patrón estrangulador)

Historia y contexto

El strangler pattern fue popularizado por Martin Fowler como una estrategia para abordar la modernización de aplicaciones monolíticas. El término “strangler” hace referencia a una planta, la higuera que crece alrededor de un árbol anfitrión y eventualmente lo "estrangula" reemplazando sus funciones hasta que el árbol original desaparece. En software, esta analogía describe cómo las nuevas funcionalidades reemplazan gradualmente las partes del sistema monolítico hasta que el antiguo sistema ya no es necesario.

Descripción técnica

El strangler pattern permite migrar de manera incremental un sistema monolítico a una arquitectura de microservicios. En lugar de realizar una reescritura completa del sistema de una vez, se migra funcionalidad por funcionalidad. Se utiliza una capa de proxy o API Gateway para redirigir las solicitudes a los microservicios si la funcionalidad ya ha sido migrada, mientras que las solicitudes que aún dependen del monolito se siguen procesando de la manera tradicional.

Estrategia de implementación

Ventajas

Desafíos

Consideraciones adicionales

Veamos un ejemplo sencillo:

Supongamos que trabajamos en una aplicación de eCommerce que gestiona usuarios. En el sistema monolítico, la funcionalidad de gestión de usuarios está fuertemente acoplada a otras partes del sistema. Usaremos el strangler pattern para migrar esta funcionalidad.

Monolito original: el controlador original que gestiona usuarios.

@RestController
@RequestMapping("/users")
public class UserController {
    @GetMapping("/{id}")
    public User getUser(@PathVariable String id) {
        // Lógica del monolito para obtener un usuario
        return userService.getUserById(id);
    }
}

Desarrollo del microservicio de usuarios: creamos un microservicio que será independiente del monolito. Este se despliega como un servicio separado que realiza la misma tarea pero de manera desacoplada.

@RestController
@RequestMapping("/api/users")
public class UserMicroserviceController {
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable String id) {
        // Obtener el usuario desde la base de datos desacoplada
        return ResponseEntity.ok(userService.findById(id));
    }
}

Configuración del API Gateway: el proxy o gateway decide si dirigir la solicitud al monolito o al microservicio. En este caso como ejemplo, usamos Spring Cloud Gateway.

@SpringBootApplication
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user_route", r -> r.path("/users/**")
                .filters(f -> f.rewritePath("/users/(?<segment>.*)", "/api/users/${segment}"))
                .uri("http://localhost:8081")) // URL del microservicio de usuarios
            .build();
    }
}

Diagrama del ejemplo:

customer --> api gateway --> user microservices (migrated) / monolite (no migrated)

Una vez que se migra la funcionalidad a la arquitectura de microservicios, todas las llamadas serán enrutadas hacia el nuevo endpoint y se desactivará o eliminará la funcionalidad migrada.

Patrón anticorrupción (Anti-Corruption Layer)

Historia y contexto

El patrón anticorrupción surge del enfoque de Domain-Driven Design (DDD) desarrollado por Eric Evans. Su objetivo es mantener los sistemas nuevos libres de las malas decisiones de diseño o modelos inconsistentes presentes en sistemas legados. Esto es particularmente importante en la migración de monolitos complejos hacia microservicios, donde es crucial evitar que los microservicios hereden las complejidades del sistema legado.

Descripción técnica

El patrón anticorrupción introduce una capa de traducción entre los microservicios nuevos y el sistema legado. Esta capa actúa como un “adaptador” que convierte las estructuras de datos y las lógicas del sistema legado en formas que son más manejables y coherentes para los microservicios.

Estrategia de implementación

Ventajas

Desafíos

Ejemplo: comunicación con un sistema de inventarios legado

Supongamos que tienes un sistema monolítico antiguo que gestiona los inventarios y estás creando un microservicio de gestión de pedidos que necesita acceder a esta información de inventarios.

Microservicio de pedidos:

@RestController
@RequestMapping("/orders")
public class OrderController {
    @Autowired
    private InventoryAntiCorruptionLayer inventoryLayer;

    @PostMapping
    public ResponseEntity<String> placeOrder(@RequestBody Order order) {
        if(inventoryLayer.checkAvailability(order.getProductId())) {
            // Lógica para procesar el pedido
            return ResponseEntity.ok("Order placed successfully");
        } else {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Product not available");
        }
    }
}

Capa anticorrupción:

La capa anticorrupción es la encargada de traducir las peticiones del microservicio de pedidos al sistema monolítico legado de inventarios.

public class InventoryAntiCorruptionLayer {
    public boolean checkAvailability(String productId) {
        // Traducir la consulta al formato del sistema legado
        LegacyInventorySystem legacySystem = new LegacyInventorySystem();
        return legacySystem.isProductAvailable(productId);
    }
}

En este caso, la clase InventoryAntiCorruptionLayer actúa como un adaptador, interactuando con el sistema de inventarios heredado (representado por la clase LegacyInventorySystem), manteniendo así al microservicio aislado de las complejidades del sistema legado.

Diagrama del ejemplo:

Cliente: Usuario que realiza las solicitudes API Gateway: Enruta las solicitudes Microservicio de Inventario: Gestiona las operaciones de inventario. ACL Traduce las solicitudes al sistema legacy Sistema de Inventario Legacy: Sistema monolítico

Cliente: usuario que realiza las solicitudes.

API Gateway: enruta las solicitudes.

Microservicio de inventario: gestiona las operaciones de inventario.

ACL: traduce las solicitudes al sistema legacy.

Sistema de inventario legacy: sistema monolítico.

Descomposición por dominio

Historia y contexto

La descomposición por dominio está profundamente ligada a la metodología Domain-Driven Design (DDD), que propone que el diseño de software debe estar alineado con el modelo de negocio de la empresa.

En lugar de basar la arquitectura en tecnologías o capas técnicas (como bases de datos, interfaces o servicios), se sugiere dividir el sistema según las necesidades y procesos de negocio, lo que ayuda a evitar el desarrollo de sistemas con dependencias intrincadas entre componentes.

Descripción técnica

La descomposición por dominio implica dividir el sistema monolítico en microservicios que correspondan a dominios específicos del negocio. Un dominio representa una parte lógica del negocio que tiene un conjunto coherente de reglas y procesos. Esta técnica permite que cada microservicio sea dueño de su lógica de negocio y persistencia de datos, eliminando dependencias innecesarias entre componentes.

Estrategia de implementación

Ventajas

Desafíos

Ejemplo: división de un sistema eCommerce

Consideremos un sistema de eCommerce monolítico que maneja pedidos, pagos y productos.

Usando descomposición por dominio, podemos dividirlo en tres microservicios:

  1. Microservicio de gestión de productos:
@RestController
@RequestMapping("/products")
public class ProductController {
    @Autowired
    private ProductService productService;

    @GetMapping("/{id}")
    public Product getProduct(@PathVariable String id) {
        return productService.findById(id);
    }

    @PostMapping
    public Product createProduct(@RequestBody Product product) {
        return productService.save(product);
    }
}
  1. Microservicio de gestión de pedidos:
@RestController
@RequestMapping("/orders")
public class OrderController {
    @Autowired
    private OrderService orderService;

    @PostMapping
    public Order placeOrder(@RequestBody Order order) {
        // Verificar disponibilidad de producto
        Product product = productClient.getProduct(order.getProductId());
        if(product != null) {
            return orderService.save(order);
        } else {
            throw new ProductNotFoundException("Producto no disponible");
        }
    }
}
  1. Microservicio de pagos:
@RestController
@RequestMapping("/payments")
public class PaymentController {
    @Autowired
    private PaymentService paymentService;

    @PostMapping
    public Payment processPayment(@RequestBody Payment payment) {
        return paymentService.processPayment(payment);
    }
}

Diagrama del ejemplo:

legacy ecommere  → users microservice, inventory microservice, orders microservice, payments microservice

Un momento… ¿strangler y descomposición por dominio no son muy parecidos?

Pues sí, se parecen y estos son sus puntos en común

Objetivo de migración gradual. Ambos patrones están orientados a una transición gradual desde una arquitectura monolítica hacia microservicios. Permiten transformar el sistema sin interrumpir las operaciones, lo cual es clave para sistemas en producción.

Mantenimiento de funcionalidad existente. Tanto el strangler como la descomposición por dominio buscan preservar las funcionalidades del sistema original mientras se va migrando. Esto ayuda a reducir el riesgo de una migración grande y única.

Estrategias de componentización. Ambos patrones buscan crear componentes que se puedan implementar y actualizar independientemente. Esto los convierte en estrategias flexibles para equipos que desean manejar y desplegar partes de la aplicación por separado.

Escalabilidad y reducción del acoplamiento. Al ir migrando servicios o dominios específicos, ambos patrones mejoran la escalabilidad y reducen el acoplamiento en la arquitectura, permitiendo escalar solo las partes necesarias.

Pero no son iguales, y aquí están sus diferencias

Propósito principal y arquitectura original.

El patrón strangler tiene como objetivo separar y reemplazar funcionalidades específicas del sistema monolítico de forma controlada y progresiva. Cada funcionalidad se extrae y sustituye con el tiempo.
El patrón de descomposición por dominio se centra en dividir la aplicación en microservicios según los dominios lógicos, permitiendo que cada servicio sea autónomo en su dominio. Se realiza una reestructuración más basada en la lógica de negocio.

Forma de evolución.

El patrón strangler se enfoca en "estrangular" o encapsular componentes del sistema monolítico, migrando funcionalidad a nuevos servicios hasta reemplazar completamente el sistema original.
El patrón de descomposición por dominio divide el sistema en servicios de acuerdo a contextos delimitados (bounded contexts), típicos de una arquitectura de dominio (DDD). Los microservicios representan entidades de negocio específicas.

Orden de migración.

El patrón strangler funciona como una serie de etapas: primero rodea el monolito, luego mueve funcionalidades individuales al nuevo sistema.
El patrón de descomposición por dominio se organiza en torno a dominios de negocio definidos y empieza con aquellos que tienen menos dependencias o pueden ofrecer valor independiente.

Compatibilidades con el monolito.

En el patrón strangler se mantienen algunas funcionalidades en el monolito durante la transición y se puede integrar con el monolito hasta que se haya reemplazado todo.
En el patrón de descomposición por dominio los microservicios en general no dependen del monolito y son autónomos en su lógica de dominio, interactuando entre ellos a través de API.

Conclusión

Estos patrones de migración, cuando se implementan correctamente, pueden hacer que la transición de un sistema monolítico a una arquitectura de microservicios sea más eficiente, escalable y modular. Sin embargo, cada patrón tiene sus propias ventajas y desafíos, por lo que es esencial que la elección de la estrategia de migración esté alineada con las necesidades y capacidades específicas del negocio.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete