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, migración, 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:

Características y Ventajas del Patrón CQRS:

Desafíos del Patrón CQRS:

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:

Componentes del Patrón BFF:

Ventajas del Patrón BFF:

Desafíos del Patrón BFF:

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:

Características y ventajas de Outbox:

Desafíos del Patrón Outbox:

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.

Suscríbete