Empezamos la sexta entrega de esta serie de post de patrones de arquitectura. Por si te has perdido alguno, aquí puedes echarle un vistazo a los artículos anteriores:

  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).

Vamos a finalizar la sección de Comunicación y coordinación entre microservicios. Lo haremos viendo CQRS (Command Query Responsibility Segregation), BFF (Backend for Frontend) y Outbox.

Pero la serie no termina aquí, ya que queda mucho contenido para siguientes publicaciones, donde veremos muchos más patrones como los relacionados con la escalabilidad y gestión de recursos con escalado automático, migración, configuración externalizada, Consumer-Driven Contract Testing, seguridad, etc…

Comunicación y coordinación entre microservicios

CQRS (Command Query Responsibility Segregation)

El patrón CQRS (Command Query Responsibility Segregation) es un patrón de diseño arquitectónico que propone separar la responsabilidad de la lectura (query) de la responsabilidad de la escritura (command) en una aplicación. Esta separación permite optimizar cada una de estas operaciones de manera independiente, lo que puede conducir a un sistema más escalable, flexible y fácil de mantener.

Componentes del Patrón CQRS:

  • Command: Representa una operación de escritura o modificación de datos en el sistema. Los comandos son responsables de realizar cambios en el estado del sistema.
  • Query: Representa una operación de lectura o consulta de datos en el sistema. Las consultas son responsables de recuperar datos del sistema sin realizar modificaciones en su estado.
  • Modelo de Datos de Lectura (Read Model): Es un modelo de datos optimizado para operaciones de lectura y consulta. Puede estar diseñado específicamente para satisfacer las necesidades de consulta de la aplicación y puede contener datos agregados o precalculados para mejorar el rendimiento de las consultas.
  • Modelo de Datos de Escritura (Write Model): Es un modelo de datos optimizado para operaciones de escritura y modificación. Puede estar diseñado de manera diferente al modelo de datos de lectura y puede optimizarse para operaciones de escritura eficientes.

Características y Ventajas del Patrón CQRS:

  • Optimización de Operaciones y Escalabilidad: Divide las operaciones de lectura (consultas) de las operaciones de escritura (comandos), lo que facilita la escalabilidad del sistema, ya que cada una puede ser escalada independientemente según sea necesario.
  • Mejora del Rendimiento: Se pueden aplicar técnicas de optimización específicas para mejorar el rendimiento de cada una, lo que conduce a una mejor experiencia del usuario.
  • Modelado de Dominio Rico: CQRS permite un modelado de dominio más rico y expresivo, ya que cada modelo de datos puede ser diseñado específicamente para satisfacer las necesidades de las operaciones que soporta.
  • Flexibilidad en la Evolución: Permite que cada parte del sistema evolucione de manera independiente, lo que facilita la introducción de cambios sin afectar a otras partes del sistema.
  • Seguridad y Control: Se puede aplicar un control de acceso más granular, lo que mejora la seguridad del sistema.
  • Claridad en el Diseño: La separación de responsabilidades vemos que afecta mucho, lo que puede hacer que el diseño del sistema sea más claro y comprensible.

Desafíos del Patrón CQRS:

  • Complejidad de Implementación: La separación de responsabilidades puede introducir una mayor complejidad en el diseño y la implementación del sistema, especialmente en sistemas distribuidos.
  • Consistencia de Datos: Mantener la consistencia entre los modelos de datos de lectura y escritura puede ser un desafío, especialmente en sistemas distribuidos, donde los datos pueden estar replicados en múltiples ubicaciones.
  • Coherencia de la Interfaz de Usuario: Al dividir las operaciones de lectura y escritura, puede ser necesario sincronizar la interfaz de usuario para garantizar una experiencia coherente para el usuario, lo que puede añadir complejidad al desarrollo.
  • Comunicación entre Componentes: La comunicación entre los componentes de lectura y escritura puede ser compleja, y puede requerir el uso de patrones de mensajería para garantizar una comunicación efectiva y confiable.
  • Gestión de Transacciones: La gestión de transacciones puede ser más compleja en un sistema CQRS debido a la separación de modelos de datos y operaciones de lectura y escritura.
  • Mayor Overhead: Puede introducir un mayor overhead debido a la necesidad de gestionar y sincronizar modelos de datos separados, lo que puede afectar el rendimiento del sistema en algunos casos.

En resumen, el patrón CQRS es una técnica útil para mejorar el rendimiento, la escalabilidad y la flexibilidad de un sistema al separar las operaciones de lectura y escritura. Sin embargo, también introduce desafíos adicionales en términos de complejidad y consistencia de datos, que deben ser considerados cuidadosamente al aplicar este patrón en un sistema.

En este diagrama se muestran los siguientes procesos:

  1. La empresa interactúa con la aplicación mediante el envío de comandos a través de una API. Los comandos son acciones como crear, actualizar o eliminar datos.
  2. La aplicación procesa el comando entrante desde el lado de los comandos. Esto implica validar, autorizar y ejecutar la operación.
  3. La aplicación conserva los datos del comando en la base de datos de escritura (comandos).
  4. Una vez que el comando se almacena en la base de datos de escritura, se activan eventos para actualizar los datos de la base de datos de lectura (consulta).
  5. La base de datos de lectura (consulta) procesa y conserva los datos. Las bases de datos de lectura están diseñadas para optimizarse para requisitos de consulta específicos.
  6. La empresa interactúa con las API de lectura para enviar consultas a la parte de consultas de la aplicación.
  7. La aplicación procesa la consulta entrante en el lado de la consulta y recupera los datos de la base de datos de lectura.

Aquí un ejemplo con código:

Supongamos que tenemos una aplicación de gestión de productos, donde los usuarios pueden agregar nuevos productos y también consultar la lista de productos disponibles.

Primero, definimos un command para agregar un nuevo producto:

public class AddProductCommand {
    private String name;
    private double price;

    // Constructor, getters y setters
}

Luego, creamos un controlador para manejar los comandos de escritura:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductCommandController {

    private final ProductCommandService productCommandService;

    @Autowired
    public ProductCommandController(ProductCommandService productCommandService) {
        this.productCommandService = productCommandService;
    }

    @PostMapping("/products")
    public void addProduct(@RequestBody AddProductCommand command) {
        productCommandService.addProduct(command);
    }
}

El servicio de commands maneja la lógica de los comandos de escritura:

import org.springframework.stereotype.Service;

@Service
public class ProductCommandService {

    public void addProduct(AddProductCommand command) {
        // Lógica para agregar un nuevo producto
        System.out.println("Nuevo producto agregado: " + command.getName());
        // Aquí se realizaría la escritura en la base de datos o en cualquier otro almacenamiento
    }
}

Para las queries, definimos un DTO (Data Transfer Object) para representar la información del producto:

public class ProductDTO {
    private String name;
    private double price;

    // Constructor, getters y setters
}

Creamos un controlador para manejar las queries:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class ProductQueryController {

    private final ProductQueryService productQueryService;

    @Autowired
    public ProductQueryController(ProductQueryService productQueryService) {
        this.productQueryService = productQueryService;
    }

    @GetMapping("/products")
    public List<ProductDTO> getAllProducts() {
        return productQueryService.getAllProducts();
    }
}

El servicio de consultas maneja la lógica de las queries:

import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class ProductQueryService {

    public List<ProductDTO> getAllProducts() {
        // Lógica para obtener todos los productos
        List<ProductDTO> products = new ArrayList<>();
        // Aquí se realizaría la consulta a la base de datos o a cualquier otro almacenamiento
        // y se mapearían los resultados a objetos ProductDTO
        return products;
    }
}

En este ejemplo, separamos las operaciones de escritura (command, comando) de las operaciones de lectura (query, consulta) utilizando el patrón CQRS. Cada operación se maneja en su propio controlador y servicio, lo que permite optimizar y escalar cada tipo de operación de manera independiente.

BFF (Backend for Frontend)

El patrón BFF (Backend For Frontend) es un enfoque arquitectónico que propone la creación de backends especializados para aplicaciones frontend específicas. En lugar de tener un único backend que sirva a todas las necesidades de los diferentes clientes frontend, el patrón BFF sugiere la creación de múltiples backends especializados, cada uno diseñado para satisfacer las necesidades particulares de un cliente frontend o una interfaz de usuario específica.

Características del Patrón BFF:

  • Backends Especializados: Cada cliente frontend tiene su propio backend especializado, diseñado para satisfacer sus necesidades específicas de datos y funcionalidades.
  • Desacoplamiento: Permite el desacoplamiento entre el frontend y el backend, ya que cada frontend interactúa con un backend dedicado que proporciona la funcionalidad requerida.
  • Optimización de Experiencia de Usuario: Permite optimizar la experiencia de usuario al proporcionar backends específicamente adaptados para las necesidades y requisitos de cada cliente frontend.
  • Seguridad y Privacidad: Permite implementar políticas de seguridad y privacidad específicas para cada cliente frontend, ya que cada backend puede gestionar la autorización y autenticación de manera independiente.

Componentes del Patrón BFF:

  • Backend For Frontend (BFF): Es un backend especializado diseñado para servir a un cliente frontend específico. Proporciona los servicios y datos necesarios para la interfaz de usuario correspondiente.
  • Cliente Frontend: Es la interfaz de usuario que interactúa con el backend correspondiente. Puede ser una aplicación web, una aplicación móvil u otro tipo de cliente.

Ventajas del Patrón BFF:

  • Optimización de Experiencia de Usuario: Permite optimizar la experiencia de usuario al proporcionar backends especializados que se adaptan a las necesidades y requisitos de cada cliente frontend.
  • Desacoplamiento: Facilita el desacoplamiento entre el frontend y el backend al permitir que cada cliente frontend interactúe con un backend dedicado.
  • Seguridad y Privacidad: Permite implementar políticas de seguridad y privacidad específicas para cada cliente frontend, ya que cada backend puede gestionar la autorización y autenticación de manera independiente.
  • Escalabilidad: Facilita la escalabilidad al permitir que los backends escalen de manera independiente según las necesidades de cada cliente frontend.

Desafíos del Patrón BFF:

  • Complejidad Adicional: Puede introducir una mayor complejidad al tener múltiples backends que deben ser diseñados, implementados y mantenidos.
  • Consistencia de Datos: Requiere manejar la consistencia de datos entre los diferentes backends, lo que puede introducir desafíos adicionales en términos de sincronización y coherencia de datos.
  • Overhead de Desarrollo: Puede introducir un mayor overhead de desarrollo al requerir el desarrollo y mantenimiento de múltiples backends especializados.

En resumen, el patrón BFF es una técnica útil para optimizar la experiencia de usuario al proporcionar backends especializados que se adaptan a las necesidades y requisitos de cada cliente frontend.

Sin embargo, también introduce desafíos adicionales en términos de complejidad y overhead de desarrollo, que deben ser considerados cuidadosamente al aplicar este patrón en un sistema.

Como con el resto de patrones, veamos un ejemplo:

Para ilustrar el patrón BFF en Java, consideremos un escenario donde tenemos una aplicación web y una aplicación móvil que comparten funcionalidades comunes, pero también tienen requisitos específicos.

Utilizaremos Spring Boot para implementar los backends especializados para cada cliente frontend.

Primero, creamos un backend para la aplicación web (Web BFF):

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WebBFFController {

    @GetMapping("/web/data")
    public String getWebData() {
        // Lógica para obtener datos específicos para la aplicación web
        return "Datos para la aplicación web";
    }
}

Luego, creamos un backend para la aplicación móvil (Mobile BFF):

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MobileBFFController {

    @GetMapping("/mobile/data")
    public String getMobileData() {
        // Lógica para obtener datos específicos para la aplicación móvil
        return "Datos para la aplicación móvil";
    }
}

Ambos backends están especializados para servir a las necesidades específicas de cada cliente frontend. La aplicación web interactúa con el endpoint “/web/data” para obtener sus datos, mientras que la aplicación móvil tendrá que invocar al endpoint “/mobile/data“ para obtener los suyos.

Con esta configuración, cada cliente frontend tiene su propio backend especializado que proporciona los servicios y datos necesarios para la interfaz de usuario correspondiente, lo que permite optimizar la experiencia de usuario y mantener un desacoplamiento entre el frontend y el backend.

Outbox

El patrón Outbox es una técnica utilizada en arquitecturas distribuidas para garantizar la consistencia entre los cambios en una base de datos local y la publicación de eventos a un sistema de mensajería como Kafka, RabbitMQ o similar.

Este patrón es especialmente útil en situaciones donde se necesita garantizar la atomicidad entre la escritura en una base de datos y la publicación de eventos relacionados, como en sistemas basados en eventos o arquitecturas de microservicios.

Componentes del Patrón Outbox:

  • Base de Datos Local: Es la base de datos donde se realizan las operaciones de escritura que generan eventos. Puede ser cualquier sistema de gestión de bases de datos relacional o NoSQL.
  • Outbox: Es una tabla o colección en la base de datos local donde se registran los eventos que deben ser publicados. Cada fila o documento en el outbox representa un evento pendiente de ser procesado.
  • Proceso de Publicación de Eventos: Es un proceso que, periódicamente, escanea el outbox en busca de eventos pendientes y los publica en un sistema de mensajería.

Características y ventajas de Outbox:

  • Atomicidad: Garantiza que las operaciones de escritura en la base de datos y la publicación de eventos relacionados se realicen de manera atómica. Es decir, que ambas se completen correctamente o ninguna se complete en absoluto.
  • Consistencia: Asegura que los eventos publicados reflejen de manera precisa los cambios realizados en la base de datos, manteniendo la consistencia entre los datos y los eventos generados.
  • Durabilidad: Los eventos publicados se almacenan en un registro persistente (el "outbox"), lo que garantiza que no se pierdan en caso de fallo del sistema o de procesamiento de eventos.
  • Desacoplamiento: Permite desacoplar la lógica de negocio de la generación de eventos, ya que las operaciones de escritura en la base de datos no están directamente relacionadas con la publicación de eventos.

Desafíos del Patrón Outbox:

  • Implementación Compleja: Requiere una implementación cuidadosa para garantizar la atomicidad entre la escritura en la base de datos y la publicación de eventos, lo que puede introducir complejidad adicional.
  • Sincronización de Eventos: Es necesario garantizar la sincronización entre los eventos publicados y los cambios en la base de datos para evitar inconsistencias.
  • Escalabilidad: El proceso de publicación de eventos debe ser escalable para manejar grandes volúmenes de eventos pendientes.

En resumen, el patrón Outbox es una técnica efectiva para garantizar la consistencia y la atomicidad entre los cambios en una base de datos local y la publicación de eventos relacionados.

Aunque puede introducir complejidad adicional en la implementación, proporciona una solución robusta para sistemas basados en eventos o arquitecturas de microservicios.

Ejemplo del Patrón Outbox:

Aquí vemos un ejemplo simplificado de cómo implementar el patrón Outbox en Java utilizando Spring Boot y Kafka siguiendo el hilo del ecommerce, por ejemplo, del procesamiento de un evento de compra.

Primero, definimos una entidad que represente los eventos de compra en el outbox:

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class PurchaseEvent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long userId;
    private Long productId;
    private int quantity;

    public PurchaseEvent() {}

    public PurchaseEvent(Long userId, Long productId, int quantity) {
        this.userId = userId;
        this.productId = productId;
        this.quantity = quantity;
    }

    // Getters y setters
}

Luego, creamos un servicio para manejar la lógica de persistencia de los eventos de compra en el outbox:

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

@Service
public class PurchaseEventService {

    @Autowired
    private PurchaseEventRepository purchaseEventRepository;

    public void addPurchaseEventToOutbox(Long userId, Long productId, int quantity) {
        PurchaseEvent event = new PurchaseEvent(userId, productId, quantity);
        purchaseEventRepository.save(event);
    }

    // Otros métodos para consultar eventos pendientes, marcar eventos como procesados, etc.
}

A continuación, configuramos un componente que escanea el outbox y publica los eventos de compra en Kafka:

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

@Component
public class PurchaseEventPublisher {

    @Autowired
    private PurchaseEventRepository purchaseEventRepository;

    @Autowired
    private KafkaProducer kafkaProducer;

    @Scheduled(fixedDelay = 1000) // Ejecutar cada segundo
    public void publishPendingPurchaseEvents() {
        List<PurchaseEvent> pendingEvents = purchaseEventRepository.findAll();
        for (PurchaseEvent event : pendingEvents) {
            kafkaProducer.send("purchaseEvent", event.toString()); // Enviar evento a Kafka
            purchaseEventRepository.delete(event);
        }
    }
}

Finalmente, un ejemplo de cómo podrías registrar eventos de compra en tu aplicación de ecommerce:

import org.springframework.beans.factory.annotation.Autowired;
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 {

    @Autowired
    private PurchaseEventService purchaseEventService;

    @PostMapping("/purchase")
    public void registerPurchaseEvent(@RequestBody PurchaseRequest request) {
        purchaseEventService.addPurchaseEventToOutbox(request.getUserId(), request.getProductId(), request.getQuantity());
    }
}

En este ejemplo adaptado, cuando se realiza una solicitud POST a /purchase con los detalles de una compra, se registra un evento de compra en el outbox.

Luego, el componente PurchaseEventPublisher escanea periódicamente el outbox y envía los eventos de compra a Kafka para su posterior procesamiento.

Ejemplo del patrón Outbox en patrones de microservicios
  1. El Cliente realiza una solicitud POST a /purchase con los detalles de la compra (userId, productId, quantity).
  2. El controlador PurchaseController recibe la solicitud y llama al método registerPurchaseEvent del servicio PurchaseEventService.
  3. PurchaseEventService crea un nuevo objeto PurchaseEvent con los detalles de la compra y lo guarda en el outbox a través del PurchaseEventRepository.
Ejemplo de Purchase Event en Outbox
  1. Periódicamente, el componente PurchaseEventPublisher escanea el outbox en busca de eventos pendientes.
  2. PurchaseEventPublisher encuentra el evento de compra pendiente y lo envía a Kafka utilizando el KafkaProducer.
  3. Kafka recibe el evento de compra.
  4. Después, los consumidores interesados los recuperarían para su posterior procesamiento.

Conclusión

En resumen, una comunicación efectiva y una coordinación adecuada entre microservicios son fundamentales para garantizar el éxito, aunque no solo basta esto.

Al comprender los diferentes enfoques y herramientas disponibles, las organizaciones pueden construir sistemas más flexibles, escalables y robustos que puedan adaptarse a las demandas cambiantes del mercado y del negocio.

Aquí terminamos los patrones de comunicación y coordinación entre microservicios. Continuaremos próximamente con más patrones de otra naturaleza.

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.