Los patrones de microservicios son soluciones recurrentes a problemas comunes que surgen al diseñar, implementar y mantener arquitecturas basadas en microservicios. Estos patrones ayudan a abordar desafíos específicos y a tomar decisiones informadas para construir sistemas escalables, mantenibles y flexibles.

Ya os contamos en un post anterior qué es una arquitectura de microservicios y veíamos desde algunas más tradicionales como MVC hasta Clean Architecture, con toda la importancia que tiene hoy en día en las arquitecturas de microservicios, para terminar con el patrón Database per Microservice.

Seguimos esta serie de patrones de arquitectura de microservicios con la segunda entrega, donde vamos a continuar con los patrones de arquitectura enfocados a la organización y estructura de microservicios.

Continuamos con DDD, Clean Architecture y con arquitectura hexagonal, estos 3 patrones pueden ir de la mano, así que veremos qué nos pueden aportar en conjunto. Finalmente, veremos la arquitectura serverless en microservicios.

Organización y estructura de microservicios

DDD

El DDD, diseño orientado al dominio (Domain-Driven Design), es un enfoque para desarrollar software que se centra en modelar el dominio del problema de manera exhaustiva y enriquecer la comunicación y colaboración entre los equipos de desarrollo y los expertos del dominio, como usuarios y stakeholders.

DDD proporciona un conjunto de principios y patrones que ayudan a crear software que refleje con precisión las reglas y procesos del negocio, lo que conduce a soluciones más efectivas y mantenibles.

El núcleo de DDD se basa en las siguientes ideas clave:

  • Lenguaje ubicuo: se busca establecer un lenguaje compartido entre los equipos de desarrollo y los expertos del dominio. Los términos utilizados en el código deben coincidir con los términos utilizados por los usuarios y stakeholders.
  • Modelado del dominio: se crea un modelo del dominio que refleja las reglas y conceptos clave del negocio. Este modelo se convierte en el núcleo de la lógica de negocio y guía la estructura del software.
  • División en subdominios: el dominio se divide en subdominios más pequeños y manejables, cada uno con su propio modelo y lógica. Esto permite abordar partes complejas del negocio de manera más efectiva.
  • Agregados y entidades: los conceptos clave del dominio se modelan como entidades y agregados. Los agregados son conjuntos coherentes de entidades que son tratados como una unidad.
  • Servicios de dominio: se crean servicios de dominio para encapsular lógica compleja que no encaja en las entidades o agregados individuales.
  • Eventos de dominio: los eventos de dominio registran cambios y eventos importantes en el sistema. Estos eventos pueden desencadenar acciones en otros componentes del sistema.
  • Contextos delimitados: se definen contextos delimitados para separar los diferentes subdominios y componentes. Cada contexto tiene su propio modelo y límites claros
  • Capas de aplicación: DDD sugiere una arquitectura en capas, donde la capa de aplicación coordina la interacción entre los objetos de dominio y los servicios de aplicación.

Veamos un ejemplo:

Supongamos que estamos desarrollando un ecommerce y queremos aplicar DDD para modelar el proceso de compra.

  • Lenguaje ubicuo: usamos el lenguaje común de "Client", "Cart", "Product", "Pedido" en el código y las discusiones con expertos del dominio.
  • Modelado del dominio: creamos entidades como "Client", "Product", "Order" y "Cart", cada una con sus propias propiedades y métodos que reflejan el negocio.
  • Agregados y entidades: creamos un agregado "Order" que incluye las entidades "Client", "Product" y "Cart". Las reglas de negocio, como verificar la disponibilidad de productos, se manejan en el agregado.
  • Servicios de dominio: creamos un servicio de dominio para calcular el total del pedido, ya que esta lógica no pertenece directamente a ninguna entidad o agregado.
  • Eventos de dominio: podemos generar un evento de dominio "OrderConfirmed" cuando se completa un pedido, lo que puede desencadenar otras acciones, como notificar al cliente o actualizar el inventario.
  • Contextos delimitados: dividimos el dominio en contextos como "Gestión de Pedidos" y "Gestión de Productos", cada uno con su propio modelo y lógica.

A la hora de particionar los dominios, pueden seguir distintas estrategias (por ejemplo: Entidades lógicas Vs Procesos de negocio) y tener diferentes consideraciones (Por ejemplo: Los subdominios del negocio global, sus características relacionadas, equipo de desarrollo, familia tecnológica, limitación de dependencias…)

DDD ofrece una forma estructurada de abordar problemas complejos de dominio y brinda claridad en la comunicación entre equipos de desarrollo y expertos del negocio. El enfoque en el modelado del dominio y la colaboración entre diferentes partes interesadas contribuyen a la creación de software de alta calidad y más alineado con las necesidades del negocio.

Aquí, un diagrama del ejemplo simplificado y acotado, se podría adaptar y expandir métodos y eventos según las necesidades de la aplicación de e-commerce y del modelo de dominio específico.

Se pueden observar los distintos dominios que a priori saldrían con sus entidades.

Distintos dominios que a priori saldrían con sus entidades.

Arquitectura hexagonal

La arquitectura hexagonal propone que nuestro dominio sea el núcleo de las capas y que este no se acople a nada externo. En lugar de hacer uso explícito y mediante el principio de inversión de dependencias, nos acoplamos a contratos (interfaces o puertos) y no a implementaciones concretas.

También conocida como arquitectura de puertos y adaptadores, el diseño se basa en la separación de las preocupaciones y la independencia de las capas. Se busca aislar el núcleo de la aplicación, donde reside la lógica de negocio, de las interacciones con componentes externos como interfaces de usuario y bases de datos.

A grandes rasgos, y sin mucho detalle, lo que propone es que nuestro núcleo sea visto como una API con unos contratos bien especificados. Definiendo puertos o puntos de entrada e interfaces (adaptadores) para que otros módulos (UI, BBDD, Test) puedan implementarlas y comunicarse con la capa de negocio sin que esta deba saber el origen de la conexión.

En un e-commerce, esta arquitectura es esencial para garantizar la flexibilidad, mantenibilidad y evolución del sistema.

Esto es lo llamado puertos y adaptadores, que podrían ser definidos de la siguiente manera:

  • Puerto: definición de una interfaz pública.
  • Adapter: especialización de un puerto para un contexto concreto.

Componentes clave:

  • Núcleo del dominio: aquí se encuentra la lógica central del negocio, incluyendo las entidades, agregados, reglas y casos de uso específicos del e-commerce.
  • Puertos. Los puertos son interfaces que definen cómo el núcleo interactúa con componentes externos. En un e-commerce:
    • Puerto de repositorio de productos: define cómo se acceden y almacenan los productos.
    • Puerto de servicio de pagos: Define cómo se manejan los pagos de los pedidos.
  • Adaptadores: los adaptadores implementan los puertos y conectan el núcleo con componentes externos:
    • Adaptador de base de datos de productos: implementa el puerto de repositorio de productos, interactuando con la base de datos.
    • Adaptador de pasarela de pagos: Implementa el puerto de servicio de pagos, conectándose con proveedores externos de pagos.
  • Interfaces de usuario: las interfaces de usuario permiten a los usuarios interactuar con el sistema:
    • Interfaz de usuario web: Permite a los clientes ver productos, agregarlos al carrito y realizar pedidos.

Un ejemplo de código sería:

  1. Puerto

Puerto de servicio de gestión de productos:

public interface ProductService {
    Product createProduct(ProductDTO productDTO);
    List<Product> getAllProducts();
}
  1. Núcleo del dominio

Entidad de producto:

public class Product {
    private String id;
    private String name;
    private double price;

    // Constructores, getters y setters
}

DTO para creación de productos:

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

    // Constructores, getters y setters
}
  1. Adaptador

Controlador de productos:

@RestController
@RequestMapping("/products")
public class ProductController {
    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody ProductDTO productDTO) {
        Product createdProduct = productService.createProduct(productDTO);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdProduct);
    }

    @GetMapping
    public ResponseEntity<List<Product>> getAllProducts() {
        List<Product> products = productService.getAllProducts();
        return ResponseEntity.ok(products);
    }
}
  1. Implementación de servicio
@Service
public class ProductServiceImpl implements ProductService {
    private final ProductRepository productRepository;

    public ProductServiceImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Override
    public Product createProduct(ProductDTO productDTO) {
        Product product = new Product();
        product.setName(productDTO.getName());
        product.setPrice(productDTO.getPrice());

        // Lógica adicional, validaciones, etc.

        return productRepository.save(product);
    }

    @Override
    public List<Product> getAllProducts() {
        return productRepository.findAll();
    }
}
  1. Importante la organización

Un buen ejemplo de cómo tendría que organizarse en paquetes sería:

Organización en paquetes

Hexagonal+DDD+Clean Architecture

Aplicar la arquitectura hexagonal, el Domain-Driven Design (DDD) y la Clean Architecture conjuntamente puede resultar en un diseño de software altamente eficiente y orientado al negocio.

Vamos a explorar cómo estos tres enfoques se combinan para crear una estructura sólida y modular.

  • La arquitectura hexagonal se centra en separar el núcleo de la lógica de negocio de las interacciones externas, como interfaces de usuario y bases de datos. La clave aquí es definir interfaces claras (puertos) a través de los cuales el núcleo se comunica con los componentes externos, y luego implementar adaptadores que conecten esas interfaces con la infraestructura externa. Esto garantiza una independencia bidireccional y permite que los cambios en una parte del sistema no afecten a las otras.
  • DDD se centra en capturar y modelar el conocimiento del negocio en el código. Encaja perfectamente con la arquitectura hexagonal, ya que el núcleo del dominio se encuentra en el centro de la arquitectura y se mantiene completamente independiente de los detalles de la implementación externa. Las entidades, agregados, objetos de valor y servicios del dominio forman el núcleo del negocio y se modelan de acuerdo con el lenguaje del dominio.
  • La Clean Architecture establece una estructura en capas, con el núcleo del negocio en el centro y las capas externas representando los detalles de implementación. La capa central contiene la lógica de negocio pura e independiente de tecnologías externas. Las capas externas, como adaptadores y frameworks, interactúan con el núcleo a través de interfaces definidas. Esto asegura que el núcleo no dependa de detalles técnicos y pueda evolucionar sin problemas.

Relación entre arquitectura hexagonal, DDD y Clean Architecture

La arquitectura hexagonal y la Clean Architecture comparten principios de diseño similares. Ambas enfatizan la separación de las responsabilidades y la creación de un código flexible y mantenible.

La arquitectura hexagonal y DDD se complementan bien, ya que DDD proporciona una guía sólida para modelar el núcleo del dominio, mientras que la arquitectura hexagonal ayuda a implementar esa lógica de dominio en un diseño flexible y desacoplado.

En resumen, estas tres metodologías y enfoques se entrelazan para crear sistemas de software que son comprensibles, mantenibles y adaptables, al tiempo que reflejan fielmente las complejidades y reglas de negocio del dominio que abordan.

Aplicación conjunta

Al combinar estos enfoques, obtienes:

  1. Independencia total. Ell núcleo del dominio está completamente aislado de las capas externas, lo que permite cambios en las tecnologías sin afectar el negocio y viceversa.
  2. Modelado de dominio. DDD permite representar las reglas del negocio y los conceptos clave del dominio en el código, creando un lenguaje compartido entre desarrolladores y expertos en el negocio.
  3. Separación de preocupaciones. La separación de capas y la definición de interfaces claras aseguran una clara separación de las preocupaciones y una estructura modular.
  4. Mantenibilidad. La estructura modular y la independencia de las tecnologías externas facilitan la evolución y el mantenimiento del sistema con el tiempo.

En resumen, aplicar conjuntamente la arquitectura hexagonal, el Domain-Driven Design y la Clean Architecture proporciona una base sólida para el diseño de software. Esta combinación fomenta la flexibilidad, el enfoque en el negocio y la capacidad de adaptación, lo que resulta en aplicaciones robustas y mantenibles a largo plazo.

Serverless

La tecnología de computación serverless ha transformado la forma en que las aplicaciones se despliegan y escalan en la nube.

Aunque el término "serverless" puede ser un poco engañoso (ya que sigue habiendo servidores en funcionamiento), se refiere a una abstracción de la infraestructura subyacente que permite a los desarrolladores enfocarse en el código y la lógica de su aplicación, en lugar de administrar servidores y recursos.

¿Qué es la computación serverless?

La computación serverless es un modelo de desarrollo y despliegue en la nube en el cual el proveedor de la nube es responsable de gestionar la infraestructura subyacente.

Los desarrolladores simplemente cargan su código en la nube y la plataforma se encarga de ejecutarlo en respuesta a eventos, como solicitudes HTTP, cambios en bases de datos o cargas en colas.

Esto permite que las aplicaciones escalen automáticamente según la demanda, sin necesidad de preocuparse por el aprovisionamiento de servidores.

Ventajas de la computación serverless:

  • Ahorro de tiempo y esfuerzo. Los desarrolladores se liberan de tareas de administración de servidores, parches y escalabilidad, lo que les permite concentrarse en escribir código y desarrollar características.
  • Escalabilidad automática. Las aplicaciones serverless pueden escalar automáticamente en función de la demanda. Los recursos se asignan automáticamente según sea necesario.
  • Costos eficientes. Al pagar solo por el tiempo de ejecución real, la computación serverless puede ser más económica en comparación con la infraestructura tradicional donde se deben mantener servidores en funcionamiento todo el tiempo.
  • Desarrollo rápido. La abstracción de la infraestructura permite a los desarrolladores lanzar y probar nuevas características de manera más rápida.
  • Enfoque en el negocio. Los equipos pueden concentrarse en desarrollar soluciones para los problemas reales del negocio en lugar de preocuparse por la infraestructura.

Casos de uso:

  1. Aplicaciones web.

Serverless es ideal para aplicaciones web con carga variable, ya que pueden escalar automáticamente según el tráfico.

  1. Procesamiento de datos.

Procesar datos en lotes o en tiempo real es eficiente con serverless, ya que puedes ejecutar funciones específicas para cada tarea.

  1. Microservicios.

Cada microservicio puede ser implementado como una función serverless, lo que facilita el mantenimiento y la escalabilidad independiente.

  • Escalado automático: Uno de los mayores beneficios de la computación serverless es su escalado automático. Cuando un evento desencadena la ejecución de una función, la plataforma serverless ajusta automáticamente los recursos necesarios para manejar la carga. Esto es esencial para lidiar con fluctuaciones en el tráfico de ecommerce, como picos de ventas durante promociones.
  • Clusters y orquestación: Aunque la computación serverless a menudo se asocia con microservicios individuales, es importante recordar que múltiples funciones serverless pueden trabajar juntas como un sistema distribuido. Aquí es donde entra en juego la orquestación de eventos, que puede ser administrada por herramientas como Amazon Step Functions o Azure Logic Apps.
    • Autodescubrimiento: En un entorno serverless, no necesitas preocuparte tanto por el autodescubrimiento de servicios, ya que las funciones serverless se activan en respuesta a eventos específicos.
    • Circuit Breaker: Si una función falla repetidamente debido a un problema, un circuit breaker podría evitar que se siga invocando, lo que protege el sistema en su conjunto.

Desafíos:

  • Duración de la ejecución: las funciones serverless tienen límites de tiempo de ejecución, lo que puede ser un desafío para tareas que requieren mucho tiempo.
  • Comunicación entre funciones: la comunicación entre funciones puede requerir una planificación cuidadosa, ya que cada función se ejecuta de forma aislada.

Ejemplo de computación serverless:

Supongamos que tienes una tienda en línea y deseas realizar una acción cada vez que se realiza un pedido, como enviar un correo electrónico de confirmación al cliente y actualizar el inventario. Puedes aprovechar la computación serverless para manejar estas acciones de manera eficiente.

// Función Lambda para procesar pedidos
exports.handler = async (event) => {

    // Parsear el evento (que podría ser un pedido en formato JSON)
    const order = JSON.parse(event.body);

    // Simular el envío de un correo electrónico de confirmación
    await sendConfirmationEmail(order.customerEmail, order.orderId);

    // Actualizar el inventario
    await updateInventory(order.products);

    // Respuesta exitosa
    return {
        statusCode: 200,
        body: JSON.stringify({ message: 'Pedido procesado exitosamente' })
    };

};

// Función para enviar un correo electrónico de confirmación
async function sendConfirmationEmail(email, orderId) {

    // Lógica para enviar el correo electrónico
    console.log(`Correo electrónico de confirmación enviado a ${email} para el pedido ${orderId}`);

}

// Función para actualizar el inventario
async function updateInventory(products) {

    // Lógica para actualizar el inventario de los productos
    console.log('Inventario actualizado para los productos:', products);

}

¿Cómo funciona?

  1. Un cliente realiza un pedido en tu tienda en línea.
  2. Tu aplicación crea un evento que contiene los detalles del pedido y lo envía a la función Lambda.
  3. La función Lambda procesa el pedido, envía un correo electrónico de confirmación al cliente y actualiza el inventario de productos.
  4. La función Lambda devuelve una respuesta indicando que el pedido ha sido procesado.

Este ejemplo muestra cómo puedes aprovechar la computación serverless para manejar tareas específicas en tu aplicación de ecommerce sin la necesidad de mantener servidores en funcionamiento todo el tiempo. Esto ahorra recursos y permite que te enfoques en brindar una excelente experiencia al cliente.

¿Qué os ha parecido? ¿Tenéis alguna duda? Podéis dejarnos un comentario. 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.