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 (enlace disponible en breve) 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

Ventajas del patrón SAGA

Desafíos que presenta el patrón SAGA

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:

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;
    }
}
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;
    }
}
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;
    }
}
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;
    }
}
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

Ventajas que ofrece

Desafíos del patrón API Gateway

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
  1. Consulta de servicios
  1. Actualización dinámica
Descubrimiento de servicios

Componentes clave:

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)

@SpringBootApplication
@EnableDiscoveryClient
public class ProductServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductServiceApplication.class, args);
    }
}
@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.

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

Estamos comprometidos.

Tecnología, personas e impacto positivo.