Continuamos con la serie de posts de patrones de microservicios. En anteriores entregas veíamos qué es una arquitectura de microservicios y patrones de arquitectura enfocados a la organización y estructura de microservicios.

En este nuevo artículo vamos a hablar de aquellos patrones que solucionan problemas de comunicación y coordinación entre microservicios.

En el contexto de las arquitecturas de microservicios, los patrones de comunicación y coordinación juegan un papel esencial al abordar los desafíos inherentes a la gestión eficiente de servicios distribuidos.

Estos patrones proporcionan soluciones recurrentes a problemas específicos, garantizando la coherencia, confiabilidad y eficacia operativa de sistemas complejos compuestos por numerosos microservicios.

Veremos orquestación de servicios, coreografía de servicios, API Gateway, descubrimiento de servicios, event Sourcing, CQRS (Command Query Responsibility Segregation), BFF (Backend for Frontend), SAGA y alguno más...

Algunos de estos vienen proporcionados ya en soluciones PaaS y no es necesario implementarlo explícitamente.

Empezaremos por dos patrones que siendo muy diferentes, se entenderán mejor si los explicamos seguidos: comunicación y coordinación entre microservicios.

Orquestación de servicios

La orquestación de servicios es un patrón de arquitectura de microservicios en el que un componente central, llamado orquestador, coordina la ejecución de varios servicios para lograr un objetivo específico. En lugar de que cada microservicio maneje la coordinación de extremo a extremo, esta responsabilidad recae en el orquestador.

Principales características

  • Orquestador

Un componente central que toma decisiones sobre la secuencia y el flujo de trabajo de los microservicios para cumplir con un caso de uso específico.

  • Coordinación

El orquestador coordina la ejecución de servicios individuales, asegurando que se invoquen en el orden correcto y con la lógica de negocio necesaria.

  • Comunicación sincrónica

La comunicación entre los microservicios puede ser síncrona, donde un servicio espera la respuesta del otro antes de continuar.

  • Flujo de trabajo empresarial

Ideal para casos de uso donde se requiere un flujo de trabajo empresarial complejo, como procesos de negocios que involucran a varios departamentos o servicios.

  • Transacciones coordinadas

Permite la coordinación de transacciones distribuidas, asegurando que todas las operaciones relacionadas se completen con éxito o se reviertan.

Ventajas

  • Centralización de la lógica de coordinación

La lógica de coordinación está centralizada en el orquestador, simplificando la implementación de los servicios individuales.

  • Visibilidad del flujo de trabajo

Facilita la comprensión y visualización de los flujos de trabajo empresariales completos.

  • Manejo de transacciones

Permite la coordinación de transacciones distribuidas a través de servicios heterogéneos.

Desafíos

  • Acoplamiento fuerte

Puede introducir acoplamiento entre los servicios, ya que deben cumplir con el formato y las expectativas del orquestador.

  • Punto único de fallo

El orquestador se convierte en un punto único de fallo y escala.

  • Mayor complejidad

Puede aumentar la complejidad general de la arquitectura, especialmente a medida que crecen los flujos de trabajo.

Tecnologías

Hay múltiples escenarios donde poder implementar el patrón orquestador en el mundo del desarrollo del software. En este caso tenemos: Apache Camel, Step Functions de AWS y varias herramientas del framework de Spring, tales como: Spring Cloud Data Flow, Spring Cloud Task, Spring Cloud Stream, Spring Integration, Spring Batch y Camel Spring Boot.

Ejemplo de escenario

Veamos cómo podríamos implementar el “ejemplo de escenario” visto anteriormente en pasos: “Proceso de Compra en un Ecommerce”.

  1. Usuario coloca una orden: un servicio de OrderService recibe la solicitud de compra.
  2. Validación de inventario: el orquestador coordina la invocación de un servicio de InventoryService para verificar la disponibilidad de productos.
  3. Procesamiento de pago: luego, coordina con un servicio de PaymentService para procesar el pago.
  4. Envío de productos: finalmente, coordina con un servicio de ShippingService para organizar el envío.
  5. Confirmación y actualización del estado: después de recibir confirmaciones de todos los servicios, el orquestador actualiza el estado de la orden y notifica al usuario.

Definición de servicios

OrderService:

public class OrderService {
    public String placeOrder(String productId) {
        // Lógica para colocar una orden
        return "Order placed for product: " + productId;
    }
}

InventoryService:

public class InventoryService {
    public String checkAvailabiity(String orderId) {
        // Lógica para validar que los productos del pedido están disponibles
        return "Inventory checked for order: " + orderId;
    }
}

PaymentService:

public class PaymentService {
    public String processPayment(String orderId) {
        // Lógica para procesar el pago
        return "Payment processed for order: " + orderId;
    }
}

ShippingService:

public class ShippingService {
    public String shipOrder(String orderId) {
        // Lógica para enviar el pedido
        return "Order shipped for order: " + orderId;
    }
}

Definición del orquestador

OrderOrchestrator:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderOrchestrator {

    private final OrderService orderService;
    private final PaymentService paymentService;
    private final ShippingService shippingService;

    @Autowired
    public OrderOrchestrator(OrderService orderService, PaymentService paymentService, ShippingService shippingService) {
        this.orderService = orderService;
        this.paymentService = paymentService;
        this.shippingService = shippingService;
    }

    public String processOrder(String productId) {
        // Paso 1: Colocar la orden
        String orderId = orderService.placeOrder(productId);

        // Paso 2: Procesar el pago
        String paymentResult = paymentService.processPayment(orderId);

        // Paso 3: Enviar el pedido
        String shippingResult = shippingService.shipOrder(orderId);

        // Retornar resultados
        return "Order Processing Summary:\n" +
                "Order ID: " + orderId + "\n" +
                "Payment Result: " + paymentResult + "\n" +
                "Shipping Result: " + shippingResult;
    }
}

Este ejemplo muy simple trata de simular un escenario de compra en un ecommerce, donde un OrderOrchestrator coordina la colocación de una orden, el procesamiento del pago y el envío del pedido. Haría falta incluir un notificador para informar al usuario y por supuesto implementar la lógica, validaciones, control de errores, etc.

Coreografía de servicios

El patrón de coreografía en la arquitectura de microservicios es un enfoque descentralizado para coordinar la interacción entre servicios.

En lugar de depender de un orquestador central que controla el flujo de trabajo, cada microservicio colabora con otros emitiendo y respondiendo a eventos.

Cada servicio es autónomo y decide cómo actuar en función de los eventos que recibe, lo que lleva a un acoplamiento más bajo entre los servicios.

Principales características

  • Descentralización

En la coreografía, no hay un componente central (orquestador) que controla el flujo de trabajo.

  • Comunicación basada en eventos

La comunicación entre servicios se realiza principalmente a través de eventos. Un servicio emite un evento cuando ocurre algo significativo, y otros servicios pueden reaccionar a estos eventos.

  • Adecuado para sistemas distribuidos

La coreografía es especialmente adecuada para sistemas distribuidos, donde los servicios pueden estar en diferentes ubicaciones y escalar de manera independiente.

Ventajas

  • Menor acoplamiento

La coreografía reduce el acoplamiento entre los microservicios porque cada uno no necesita tener conocimiento directo de los detalles internos de los demás. Los servicios solo necesitan saber qué eventos deben emitir y cómo reaccionar a eventos específicos.

  • Escalabilidad

Al descentralizar la coordinación, la arquitectura de coreografía puede ser más fácilmente escalable. Cada servicio puede escalar de manera independiente según sus propias necesidades, sin depender de un orquestador central.

  • Flexibilidad

La independencia de los servicios permite cambios y actualizaciones más fáciles en cada componente. Se pueden agregar, modificar o eliminar servicios sin afectar directamente a los demás, siempre y cuando los eventos sigan siendo compatibles.

  • Desarrollo independiente:

Equipos de desarrollo independientes pueden trabajar en servicios específicos sin interferencias. La interfaz de eventos define cómo los servicios se comunican, permitiendo un desarrollo más rápido y paralelo.

Desafíos

  • Complejidad de seguimiento

A medida que la cantidad de servicios y eventos aumenta, el seguimiento y la comprensión del flujo de trabajo pueden volverse complejos. La visibilidad del flujo completo puede ser un desafío, especialmente en entornos distribuidos.

  • Orden de eventos

Asegurar el orden correcto de los eventos puede ser complicado. En algunos casos, es fundamental que ciertos eventos se procesen en un orden específico, lo que puede requerir una gestión adicional.

  • Manejo de errores

La gestión de errores puede ser más desafiante en un entorno de coreografía. Identificar y manejar errores puede requerir una estrategia robusta para garantizar la integridad y consistencia del sistema.

  • Difícil para flujos de trabajo complejos

En algunos casos, especialmente en flujos de trabajo empresariales complejos, la coreografía puede volverse difícil de mantener y entender. Un orquestador central puede ser más adecuado para flujos de trabajo altamente estructurados.

Tecnologías

Para implementar el patrón de arquitectura de coreografía es necesario una herramienta que nos permita manejar eventos. Estas son 3 herramientas muy comunes que se suelen utilizar actualmente: Apache Kafka, RabbitMQ o incluso Amazon SNS (Simple Notification Service).

Ejemplo de coreografía con Kafka en Spring

Este ejemplo utiliza Apache Kafka como el sistema de mensajería para facilitar la coreografía entre los microservicios. Cada servicio emite eventos a un topic (tema) de Kafka, y un consumidor de Kafka en otro servicio maneja esos eventos. Este enfoque permite una comunicación asincrónica y descentralizada entre los microservicios en un sistema basado en eventos.

  1. Configuración de Kafka:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.core.KafkaTemplate;

@Configuration
@EnableKafka
public class KafkaConfig {

    @Bean
    public KafkaTemplate<String, Object> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }

    // Configuración adicional del productor de Kafka (ver documentación)
    // ...
}
  1. Servicios (con productor):

OrderService:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final KafkaTemplate<String, Object> kafkaTemplate;

    @Autowired
    public OrderService(KafkaTemplate<String, Object> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void placeOrder(String productId) {
        // Lógica para colocar la orden
        String orderId = generateOrderId();

        // Emisión del evento a Kafka
        kafkaTemplate.send("order-placed", new OrderPlacedEvent(orderId));
    }

    private String generateOrderId() {
        // Lógica para generar un ID de orden
        return "ORD123";
    }
}

PaymentService:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private final KafkaTemplate<String, Object> kafkaTemplate;

    @Autowired
    public PaymentService(KafkaTemplate<String, Object> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void processPayment(String orderId) {
        // Lógica para procesar el pago

        // Emisión del evento a Kafka
        kafkaTemplate.send("payment-processed", new PaymentProcessedEvent(orderId));
    }
}

ShippingService:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class ShippingService {

    private final KafkaTemplate<String, Object> kafkaTemplate;

    @Autowired
    public ShippingService(KafkaTemplate<String, Object> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void shipOrder(String orderId) {
        // Lógica para enviar el pedido

        // Emisión del evento a Kafka
        kafkaTemplate.send("order-shipped", new OrderShippedEvent(orderId));
    }
}
  1. Consumidor:
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Component
public class KafkaEventConsumer {

    @KafkaListener(topics = "order-placed")
    public void handleOrderPlacedEvent(OrderPlacedEvent event) {
        // Lógica para manejar el evento de orden colocada
        System.out.println("Order Placed Event Received for Order ID: " + event.getOrderId());
    }

    @KafkaListener(topics = "payment-processed")
    public void handlePaymentProcessedEvent(PaymentProcessedEvent event) {
        // Lógica para manejar el evento de pago procesado
        System.out.println("Payment Processed Event Received for Order ID: " + event.getOrderId());
    }

    @KafkaListener(topics = "order-shipped")
    public void handleOrderShippedEvent(OrderShippedEvent event) {
        // Lógica para manejar el evento de orden enviada
        System.out.println("Order Shipped Event Received for Order ID: " + event.getOrderId());
    }
}

En este ejemplo, cada servicio emite eventos a un topic de Kafka correspondiente, y el consumidor de Kafka (KafkaEventConsumer) en el mismo o en otro servicio maneja esos eventos.

Este enfoque ilustra cómo los microservicios colaboran de manera descentralizada mediante eventos en un sistema basado en Kafka. Cada microservicio sigue siendo independiente y reacciona a eventos sin la necesidad de un orquestador central.

Orquestación vs. Coreografía

En orquestación, un controlador (orquestador) gestiona el flujo de interacción entre los servicios. Cada solicitud de servicio se realiza de acuerdo con un pedido y una condición específicos y el orquestador decide el flujo en función de la configuración y del resultado de otros servicios.

En coreografía, todos los servicios funcionan de forma independiente e interactúan únicamente a través de eventos compartidos y conexiones flexibles. Los servicios se suscriben solo a los eventos que son relevantes para ellos y realizan una tarea específica cuando reciben una notificación.

En el contexto de las arquitecturas basadas en eventos, el patrón más popular es la coreografía. Sin embargo, según sus requisitos, puede que no sea la mejor opción.

A la hora de saber qué patrón escoger sería conveniente tener estas consideraciones técnicas (no tienen por qué ser las únicas):

  • Complejidad del flujo de trabajo: para flujos de trabajo más simples y estructurados, la orquestación puede ser más apropiada. Para flujos de trabajo complejos y que cambian dinámicamente, la coreografía puede ofrecer más flexibilidad.
  • Visibilidad y control: si se necesita un mayor control centralizado y visibilidad, la orquestación puede ser preferible. Si se busca una arquitectura más distribuida y descentralizada, la coreografía puede ser la elección.
  • Escalabilidad: la coreografía a menudo facilita la escalabilidad porque los servicios pueden evolucionar de manera más independiente. La orquestación puede tener desafíos con la escalabilidad en entornos de servicios altamente distribuidos.

En última instancia, la elección entre orquestación y coreografía depende de la naturaleza del sistema, los requisitos específicos y las preferencias de diseño. Algunas arquitecturas pueden incluso combinar ambos patrones según las necesidades específicas de los flujos de trabajo.

Usar la orquestación y la coreografía juntas

La orquestación y la coreografía no se excluyen necesariamente entre sí y pueden funcionar juntas. En ocasiones se pueden implementar conjuntamente y dependiendo de la operación, puede ser conveniente que se integre de manera orquestal o a través de un sistema de eventos.

En algunos casos, el uso conjunto de orquestación y coreografía puede proporcionar una solución equilibrada y flexible en una arquitectura de microservicios. Esta combinación se conoce a veces como "coreografía guiada por orquestación" o "coreografía orquestada".

Algunas de las ventajas son:

  • Flexibilidad y escalabilidad: la coreografía proporciona flexibilidad y escalabilidad para interacciones autónomas, mientras que la orquestación brinda control centralizado cuando es necesario.
  • Menor acoplamiento: los servicios pueden ser autónomos y tener un menor acoplamiento, lo que facilita cambios y actualizaciones.

Y los desafíos son:

  • Complejidad de diseño: se debe realizar un diseño cuidadoso para determinar qué partes del sistema son manejadas por la orquestación y cuáles por la coreografía.
  • Coordinación: asegurar una coordinación adecuada entre orquestación y coreografía puede requerir un esfuerzo adicional.

Consideraciones importantes

  • Claridad en los roles: es crucial definir claramente los roles y responsabilidades de los componentes de orquestación y coreografía.
  • Gestión de transacciones: se debe tener en cuenta cómo manejar las transacciones entre servicios en ambos enfoques para garantizar la consistencia en el sistema.
  • Monitoreo y seguimiento: un sistema de monitoreo robusto es esencial para rastrear y analizar el comportamiento de los servicios, especialmente en un entorno distribuido.
  • Adaptabilidad: la combinación de orquestación y coreografía debe ser adaptable a medida que evolucionan los requisitos y las características del sistema.

Ejemplo: en un proceso de compra en línea, la orquestación podría manejar la verificación del carrito y el procesamiento de pagos, mientras que la coreografía podría utilizarse para la gestión de inventario y el envío de notificaciones.

La elección de usar ambos enfoques dependerá de la complejidad del sistema, los requisitos de negocio y la preferencia del equipo de desarrollo. En situaciones donde se necesite tanto control centralizado como autonomía distribuida, esta combinación puede ofrecer lo mejor de ambos mundos.

Tanto el patrón orquestador como el de coreografía son patrones que también están estrechamente relacionados con el patrón SAGA, el cual veremos en la siguiente entrada.

Además, si os interesa este tema, te dejamos por aquí los siguientes posts de la serie:

¡Esperemos que os 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.