Hace unas semanas comenzamos una serie de post de patrones de arquitectura de microservicios primero hablando de qué son, de su organización y estructura y de la comunicación y coordinación de microservicios.

Ahora, continuamos con esta serie de patrones de arquitectura de microservicios con la cuarta entrada de la serie, donde vamos a continuar con aquellos que solucionan problemas de comunicación y coordinación entre microservicios.

Todavía nos quedan bastantes que ver de este tipo, pero empezaremos con el patrón SAGA, ya que está muy relacionado con los patrones vistos recientemente: orquestación de servicios y coreografía de servicios.

Después seguiremos con Event Sourcing y EDA, dejando para el final CQRS (Command Query Responsibility Segregation), BFF (Backend for Frontend) y Outbox.

Recordamos una de las ventajas principales que nos proporcionan las arquitecturas de microservicios: la principal virtud de la arquitectura de microservicios es que es modular.

Esto significa que cada uno de los servicios del software se puede construir de forma independiente. Además, una vez que ya están funcionando, los servicios utilizan su propio proceso.

SAGA

El patrón SAGA se utiliza en sistemas distribuidos para gestionar transacciones largas y complejas de manera eficiente y consistente. Este patrón se inspira en la estructura narrativa de las sagas literarias, donde una serie de eventos están interconectados para formar una historia coherente.

En el ámbito de la informática, el patrón SAGA permite coordinar y gestionar la ejecución de múltiples operaciones que involucran varios servicios y garantizar que la transacción mantenga su consistencia global, incluso en entornos distribuidos y con fallos.

Características principales

  • Transacciones distribuidas: el patrón SAGA permite la ejecución de transacciones que involucran múltiples servicios distribuidos, cada uno de los cuales puede realizar operaciones atómicas.
  • Compensación automática: define pasos de compensación asociados con cada paso de la transacción. Estos pasos de compensación pueden revertir los efectos de las operaciones anteriores en caso de fallo, garantizando la consistencia de los datos.
  • Orquestación de transacciones: el patrón SAGA coordina la secuencia de operaciones entre los servicios participantes en una transacción, asegurando la consistencia global de los datos incluso en entornos distribuidos.
  • Tolerancia a fallos: proporciona mecanismos para manejar fallos y situaciones excepcionales durante la ejecución de la transacción, minimizando el riesgo de inconsistencia de datos.

Ventajas del patrón SAGA

  • Flexibilidad en la implementación: el patrón SAGA permite la implementación de transacciones complejas que abarcan múltiples servicios, manteniendo la coherencia de los datos en todo momento.
  • Escalabilidad y desacoplamiento: al distribuir la lógica de transacciones entre diferentes servicios, el patrón SAGA facilita la escalabilidad y reduce el acoplamiento entre componentes.
  • Mantenimiento de la consistencia: asegura que la base de datos y otros recursos involucrados en la transacción mantengan una consistencia global, incluso en presencia de fallos.
  • Auditoría y seguimiento: facilita la auditoría y el seguimiento de las transacciones, ya que cada paso y su estado pueden registrarse y analizarse fácilmente.

Desafíos que presenta el patrón SAGA

  • Complejidad de Implementación: la implementación de transacciones distribuidas y la lógica de compensación puede ser compleja y requerir un diseño cuidadoso.
  • Gestión de estados: la gestión de estados de transacción puede volverse compleja, especialmente en transacciones largas que involucran múltiples servicios.
  • Coordinación y orquestación: requiere una coordinación eficiente entre los servicios participantes para garantizar que las transacciones se ejecuten de manera correcta y consistente.
  • Rendimiento y escalabilidad: la gestión de transacciones distribuidas puede tener un impacto en el rendimiento y la escalabilidad del sistema, especialmente en entornos de alto volumen.

En resumen, el patrón SAGA ofrece una solución efectiva para gestionar transacciones distribuidas en entornos complejos, como el comercio electrónico. Aunque ofrece numerosas ventajas, su implementación puede ser desafiante y requiere un diseño cuidadoso para garantizar la consistencia y la fiabilidad del sistema.

Un ejemplo

Lo vamos a ver mucho más claro con un ejemplo. Imaginemos un sistema de comercio electrónico donde los usuarios pueden realizar compras de productos.

El proceso de compra implica varios pasos, como la reserva de productos en el inventario, el procesamiento del pago, la generación de la factura y la notificación de envío.

Utilizaremos el patrón SAGA para gestionar este proceso de manera consistente, incluso en un entorno distribuido y ante posibles fallos.

Patrón Saga
  1. Reserva de productos en el inventario

Cuando un usuario agrega productos a su carrito de compras, se inicia una transacción para reservar dichos productos en el inventario. Si el inventario tiene suficientes existencias, la reserva se confirma. Si no, se produce un fallo y se ejecuta el paso de compensación para liberar cualquier reserva realizada.

  1. Procesamiento del pago

Una vez que los productos están reservados, se procede al procesamiento del pago. Se realiza una transacción para cobrar al cliente el monto total de la compra. Si la transacción de pago se completa con éxito, se avanza al siguiente paso. En caso de fallo, se ejecuta la compensación para liberar las reservas de productos en el inventario.

  1. Generación de la factura

Después de que el pago se ha procesado con éxito, se genera una factura para el cliente. Este paso implica la creación de un registro de la transacción en la base de datos y la generación de un documento de factura. Si la generación de la factura se completa correctamente, se procede al paso final. De lo contrario, se ejecuta la compensación para revertir la transacción de pago y liberar las reservas de productos en el inventario.

  1. Notificación de envío

Finalmente, se notifica al cliente que su pedido ha sido enviado. Esta acción implica actualizar el estado del pedido y enviar una notificación por correo electrónico o mensaje de texto al cliente. Si la notificación se envía correctamente, se completa el proceso de compra. En caso de fallo, se ejecuta la compensación para revertir la generación de la factura, el procesamiento del pago y la liberación de las reservas de productos en el inventario.

  1. Compensación automática

En cada paso del proceso de compra, se define un mecanismo de compensación que puede revertir las operaciones realizadas en caso de fallo. Por ejemplo, si el procesamiento del pago falla, se ejecuta la compensación para liberar las reservas de productos en el inventario y garantizar la integridad del sistema.

Si este ejemplo lo plasmamos en código elaborado con Java y Spring Boot, a modo esquemático y sin completar la lógica sería:

  • Reserva de productos en el inventario:
import org.springframework.stereotype.Service;

@Service
public class OrderServoce {

    public boolean processOrder(Order order) {
        // Aquí va la lógica para reservar productos en el inventario
        // Si hay suficiente stock, realiza la reserva
        // Si no, lanza una excepción
        // En caso de error, se realizará automáticamente el rollback 

        return true;
    }
}
  • Procesamiento del pago:
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    public boolean processPayment(Order order) {
        // Lógica para procesar el pago
        // Si el pago se procesa correctamente, retorna true
        // Si hay un fallo, lanza una excepción
        // En caso de error, se realizará automáticamente el rollback 

        return true;
    }
}
  • Generación de la factura:
import org.springframework.stereotype.Service;

@Service
public class BillingService {

    public boolean processBilling(Order order) {
        // Lógica para generar la factura
        // Si la factura se genera correctamente, retorna true
        // Si hay un fallo, lanza una excepción
        // En caso de error, se realizará automáticamente el rollback 

        return true;
    }
}
  • Notificación de envío:
import org.springframework.stereotype.Service;

@Service
public class NotificationService {

    public boolean sendNotification(Order order) {
        // Lógica para notificar al cliente sobre el envío
        // Si la notificación se envía correctamente, retorna true
        // Si hay un fallo, lanza una excepción
        // En caso de error, se realizará automáticamente el rollback 

        return true;
    }
}
  • Controlador de compra:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PurchaseController {

    private final OrderService orderService;
    private final PaymentService paymentService;
    private final BillingService billingService;
    private final NotificationService notificationService;

    @Autowired
    public PurchaseController(OrderService orderService, PaymentService paymentService,
                              BillingService billingService, NotificationService notificationService) {
        this.orderService = orderService;
        this.paymentService = paymentService;
        this.billingService = billingService;
        this.notificationService = notificationService;
    }

    @PostMapping("/purchase")
    public ResponseEntity<String> processPurchase(@RequestBody Purchase purchase) {
        try {
             // Paso 1: Reservar productos en el inventario
            orderService.reserveProducts(purchase);
            // Paso 2: Procesar el pago
            paymentService.processPayment(purchase);
            // Paso 3: Generar factura
            billingService.generateInvoice(purchase);
            // Paso 4: Notificar el envío
            notificationService.notifyShipping(purchase);

            
            return ResponseEntity.ok("Purchase processed successfully.");
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                 .body("Error processing purchase: " + e.getMessage());
        }
    }
}

En este caso, dejamos que sea el framework de Spring quien realiza el rollback de manera automática, ya que tiene transaccionalidad.

API Gateway

El patrón de API Gateway actúa como un punto de entrada único para gestionar las solicitudes de clientes en un sistema de microservicios. Proporciona funcionalidades como enrutamiento, autenticación, autorización y agregación de datos, simplificando así la complejidad para los clientes y mejorando la eficiencia.

Principales características

  • Unificación de entrada: proporciona un punto de entrada único para los clientes, independientemente de la complejidad de los microservicios subyacentes.
  • Enrutamiento de solicitudes: dirige las solicitudes de los clientes hacia los microservicios correspondientes, basándose en criterios como la URL, el método HTTP, o los parámetros de la solicitud.
  • Gestión de autenticación y autorización: implementa mecanismos de seguridad para autenticar a los usuarios y autorizar el acceso a los recursos protegidos.
  • Control de versiones: permite la gestión de múltiples versiones de API, facilitando la evolución controlada de la arquitectura.
  • Monitoreo y métricas: ofrece capacidades para monitorear el tráfico de las solicitudes, detectar problemas de rendimiento y recopilar métricas para análisis posterior.

Ventajas que ofrece

  • Simplificación del cliente: los clientes interactúan con un único punto de entrada, lo que simplifica su implementación y reduce la complejidad del código del cliente.
  • Seguridad centralizada: concentra la lógica de seguridad en un solo lugar, facilitando la implementación y el mantenimiento de políticas de seguridad coherentes.
  • Escalabilidad independiente: permite escalar los microservicios de manera independiente, ya que el API Gateway actúa como intermediario entre los clientes y los microservicios.
  • Flexibilidad en la implementación: proporciona la flexibilidad para implementar lógica personalizada, como transformaciones de datos o agregaciones, antes de enviar las solicitudes a los microservicios.

Desafíos del patrón API Gateway

  • Cuello de botella potencial: si el API Gateway se convierte en un punto de congestión debido a un alto volumen de tráfico, puede provocar cuellos de botella en el sistema.
  • Complejidad de configuración: configurar y mantener un API Gateway puede ser complejo, especialmente en sistemas con múltiples microservicios y reglas de enrutamiento complicadas.
  • Acoplamiento excesivo: existe el riesgo de que el API Gateway se convierta en un punto de acoplamiento excesivo, lo que dificulta la evolución independiente de los microservicios.
  • Gestión de versiones: la gestión de versiones puede volverse complicada, especialmente cuando se introducen cambios que afectan a múltiples microservicios.

Un ejemplo

Supongamos que tenemos un servicio de productos y un servicio de carrito en nuestro sistema de ecommerce. El API Gateway podría exponer un endpoint /ecommerce que dirige las solicitudes a los servicios correspondientes.

@RestController
@RequestMapping("/ecommerce")
public class ApiGatewayController {

    @Autowired
    private ProductService productService;

    @Autowired
    private CartService cartService;

    @GetMapping("/products/{productId}")
    public Product getProduct(@PathVariable String productId) {
        // Enrutamiento al servicio de productos
        return productService.getProductById(productId);
    }

    @GetMapping("/cart/{userId}")
    public Cart getCart(@PathVariable String userId) {
        // Enrutamiento al servicio de carrito
        return cartService.getUserCart(userId);
    }

    @PostMapping("/cart/{userId}/add")
    public ResponseEntity<String> addToCart(@PathVariable String userId, @RequestBody CartItem item) {
        // Enrutamiento y llamada al servicio de carrito para agregar un elemento
        cartService.addToCart(userId, item);
        return ResponseEntity.ok("Item added to the cart.");
    }
}
API Gateway
  1. Enrutamiento

El API Gateway maneja las rutas de los clientes y las redirige a los microservicios correspondientes.

  1. Autenticación y Autorización

Puede integrar lógica de autenticación y autorización para proteger los microservicios subyacentes.

@GetMapping("/user/{userId}")
public ResponseEntity<User> getUser(@PathVariable String userId, @RequestHeader("Authorization") String token) {
    // Lógica de verificación de token y llamada al servicio de usuarios
    if (isValidToken(token)) {
        return ResponseEntity.ok(userService.getUserById(userId));
    } else {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
}
  1. Agregación de datos

En solicitudes complejas, puede llamar a múltiples microservicios y agregar los resultados antes de enviar una respuesta al cliente.

@GetMapping("/user/{userId}/details")
public ResponseEntity<UserDetails> getUserDetails(@PathVariable String userId) {
    // Lógica para llamar al servicio de usuario y al servicio de pedidos y combinar los datos
    User user = userService.getUserById(userId);
    List<Order> orders = orderService.getOrdersByUserId(userId);
    UserDetails userDetails = new UserDetails(user, orders);
    return ResponseEntity.ok(userDetails);
}
  1. Simplificación para el cliente

Los clientes interactúan con un único punto de entrada, lo que simplifica la experiencia de desarrollo y mejora la mantenibilidad. El API Gateway puede simplificar la experiencia del cliente al ofrecer servicios combinados, evitando que los clientes llamen a múltiples endpoints para obtener información relacionada.

  1. Escalabilidad

Permite escalar los servicios de manera independiente según las necesidades, ya que los clientes solo interactúan con el API Gateway.

  1. Gestión de versiones
@GetMapping(value = "/products/{productId}", headers = "API-Version=1")
public ResponseEntity<ProductV1> getProductV1(@PathVariable String productId) {
    // Lógica para llamar al servicio de productos con lógica de versión 1
    return ResponseEntity.ok(productService.getProductV1(productId));
}

@GetMapping(value = "/products/{productId}", headers = "API-Version=2")
public ResponseEntity<ProductV2> getProductV2(@PathVariable String productId) {
    // Lógica para llamar al servicio de productos con lógica de versión 2
    return ResponseEntity.ok(productService.getProductV2(productId));
}
  1. Documentación de la API
// Utilizando herramientas como Springfox para generar documentación Swagger
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.ecommerce"))
                .paths(PathSelectors.any())
                .build();
    }
}

Esto es un ejemplo simple de como hacerlo en Java, pero lo más común es que se realice en un sistema especializado, con herramientas llamadas API Manager, que facilitan todos los aspectos de seguridad, enrutamiento, escalabilidad, etc. Estos son algunos:

Descubrimiento de servicios

El patrón de descubrimiento de servicios es fundamental en la arquitectura de microservicios y permite a los servicios encontrar y comunicarse entre sí de manera dinámica sin depender de configuraciones estáticas.

Funcionamiento

  1. Registro de servicios
  • Cuando un servicio se inicia, se registra automáticamente en el registro de servicios.
  • El servicio envía información sobre su ubicación, capacidad y endpoints disponibles al registro.
  1. Consulta de servicios
  • Otros servicios que necesitan interactuar con un servicio específico pueden consultar el registro de servicios para obtener información sobre la ubicación y detalles del servicio deseado.
  1. Actualización dinámica
  • Si un servicio se detiene o escala horizontalmente, actualiza su entrada en el registro.
  • Los servicios consumidores pueden adaptarse dinámicamente a estos cambios sin intervención manual.
Descubrimiento de servicios

Componentes clave:

  • Registro de Servicios (Service Registry):
    • Almacén centralizado que mantiene información sobre los servicios disponibles.
    • Ejemplos populares incluyen Eureka, Consul, y etc.
  • Cliente de descubrimiento (Service Discovery Client):
    • Biblioteca o módulo integrado en cada servicio para interactuar con el registro.
    • Se utiliza para registrar un servicio al iniciar y para buscar servicios necesarios.

Ventajas del descubrimiento de servicios

  1. Autoconfiguración

La autoconfiguración permite a los servicios adaptarse automáticamente a los cambios en la infraestructura, como la adición o eliminación de instancias de servicios.

  1. Escalabilidad dinámica

Facilita la escalabilidad dinámica, ya que los servicios pueden escalar horizontalmente sin requerir cambios manuales en la configuración de otros servicios.

  1. Alta disponibilidad

Si una instancia de servicio falla, el registro de servicios puede actualizar la información, y los consumidores pueden redirigir automáticamente sus solicitudes a instancias disponibles.

  1. Despliegue continuo

Simplifica el despliegue continuo, ya que los nuevos servicios pueden registrarse automáticamente y los consumidores pueden descubrirlos sin intervención manual.

Desafíos del descubrimiento de servicios

  1. Latencia de descubrimiento

Puede haber una pequeña latencia al buscar servicios, especialmente en grandes sistemas distribuidos.

  1. Consistencia

Mantener la consistencia en el registro puede ser un desafío en entornos altamente distribuidos.

  1. Seguridad

Se deben implementar medidas de seguridad para proteger la información del registro y garantizar la integridad de las comunicaciones entre servicios.

  1. Complejidad agregada

Agrega una capa adicional de complejidad a la arquitectura general, y su introducción debe equilibrarse con los beneficios que aporta.

Ejemplo de descubrimiento de Servicios con Eureka (Spring Cloud)

  • Configuración del servicio:
@SpringBootApplication
@EnableDiscoveryClient
public class ProductServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductServiceApplication.class, args);
    }
}
  • Cliente del Servicio (Consumer):
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/placeOrder/{productId}")
    public String placeOrder(@PathVariable String productId) {
        // Utiliza el nombre del servicio registrado ("product-service") en lugar de la URL
        String productInfo = restTemplate.getForObject("http://product-service/product/" + productId, String.class);
        return "Order placed for Product ID " + productId + ". Product Info: " + productInfo;
    }
}

Consideraciones finales:

El patrón de descubrimiento de servicios es crucial para arquitecturas de microservicios, ya que permite una interacción flexible y dinámica entre servicios.

Sin embargo, debe implementarse y administrarse cuidadosamente para abordar desafíos potenciales y garantizar la fiabilidad y la seguridad en el entorno de producción.

Por otro lado, en los casos de PaaS gestionados suele ser una funcionalidad que ya nos viene dada.

Conclusiones

En resumen, hemos visto como SAGA nos sirve para la gestión de transacciones distribuidas, API Gateway para la entrada única de solicitudes y Service Discovery para la localización dinámica de servicios.

Se evidencia la importancia de adoptar estrategias eficientes en entornos distribuidos.

Estas herramientas ofrecen soluciones sólidas para abordar desafíos como la escalabilidad, la disponibilidad y la confiabilidad, al tiempo que facilitan la adaptabilidad y la mantenibilidad de los sistemas. Si quieres seguir profundizando, en este post vemos un caso de escalabilidad y gestión de recursos con escalado automático y en este, pasamos de monolitos a microservicios. También hablamos de la configuración externalizaday de Consumer-Driven Contract Testing.

¡Espero que sea útil!

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.