Arquitectura de microservicios: ejemplos y conceptos claves

7 min read
5 de julio de 2023

En el mundo del desarrollo de software, los microservicios son un enfoque de arquitectura en el que una aplicación o un sistema se divide en componentes pequeños e independientes. Cada una de estas partes, o microservicios, cumple una función específica dentro de un sistema y funcionan como una unidad autónoma y auto-contenida.

A continuación vas encontrar información útil sobre

Para profundizar más sobre el tema también puedes consultar nuestra Pragma Talk, una conferencia en la que hacemos una introducción detallada a la arquitectura de mircroservicios.

Y si lo que buscas es trabajar en una multinacional con grandes proyectos y en compañía de una comunidad de profesionales que te ayudan a crecer, consulta nuestra oferta de empleos y lleva tu carrera al siguiente nivel. Ahora sí, hablemos de las ventajas de usar la arquitectura de microservicios. 

Beneficios de utilizar arquitectura de microservicos

La arquitectura de microservicios surgió para superar algunas de las limitaciones más comunes de las arquitecturas monolíticas y las arquitecturas orientadas a servicio (SOA). Entre sus principales beneficios encontramos: 

  • Flexibilidad y resiliencia:
    Los microservicios facilitan el mantenimiento, evolución y el mejoramiento continuo de los activos digitales. Al ser unidades independientes, los microservicios pueden actualizarse y desplegarse con facilidad y sin causar problemas en otros componentes. Esta es una ventaja significativa frente a arquitecturas monolíticas en las que pequeños cambios en una parte del sistema pueden generar fallos generalizados. 

  • Escalabilidad
    Dividir una aplicación en microservicios permite que cada componente se pueda escalar de manera individual. Esto es particularmente útil si se complementa con una infraestructura en la nube pues, de esta manera, se garantiza que en todo momento el microservicio cuente con la infraestructura adecuada, independientemente de si la demanda es enorme o muy pequeña. Esto también permite optimizar costos pues la mayoría de servicios de la nube se paga únicamente por lo que se utiliza.

  • Compatibilidad con diferentes lenguajes y tecnologías

    La independencia de los microservicios nos permite utilizar distintas tecnologías, bases de datos y lenguajes de programación en cada uno de ellos. Esto es así gracias a que los microservicios pueden comunicarse entre sí a través de APIs (Interfaces de Programación de Aplicaciones) o servicios web, utilizando protocolos estándar de comunicación. 

Las arquitecturas de microservicios por lo general van de la mano con un enfoque de diseño de software conocido como Domain Driven Disign o DDD. A continuación encontrarás algunos de los conceptos clave de este enfoque que te pueden ayudar a identificar un microservicio. 

Entidades, Value Objects y Bounded Context: ¿qué son y cómo nos ayudan a identificar un microservicio?

Propuesto por Eric Evans, el DDD consiste en darle prioridad a los conceptos, términos y procesos del negocio (dominio), por encima de la estructura técnica o los requisitos funcionales. 

Cuando se usa DDD en conjunto con la arquitectura de microservicios, el resultado son activos digitales que están compuestos por servicios autónomos e independientes, que están especializados en cada área del dominio empresarial.

La experiencia es fundamental para identificar dónde están los límites entre uno y otro microservicio, sin embargo, DDD también cuenta con 4 conceptos fundamentales que nos pueden ayudar en esta tarea: 

Entidades

Representan conceptos del dominio del negocio que, por sus atributos y su comportamiento, tienen una identidad única y distinguible.

Para entenderlo mejor podemos tomar el ejemplo del objeto “Cliente” en un e-commerce.

En este caso, un cliente es una entidad con un ID único que, al mismo tiempo, tiene diferentes atributos como nombre, dirección, correo electrónico, número de teléfono, etc.  

public class Cliente {
    private String id;
    private String nombre;
    private String email;
    private List<Pedido> historialPedidos;

    public Cliente(String id, String nombre, String email) {
        this.id = id;
        this.nombre = nombre;
        this.email = email;
        this.historialPedidos = new ArrayList<>();
    }

    // Getters y setters

    public void agregarPedido(Pedido pedido) {
        historialPedidos.add(pedido);
    }

    public void eliminarPedido(Pedido pedido) {
        historialPedidos.remove(pedido);
    }
}
 

 

Value Objects: 

Los value objects también representan conceptos del dominio pero NO tienen una identidad distinguible. Están definidos por atributos pero a diferencia de las entidades, los value objects pueden igualarse a otros si el valor de esos atributos es el mismo. Veámos un ejemplo.

Es común en muchas aplicaciones la fecha de nacimiento sea considerada un concepto que requiere tratarse como un todo pero no necesita una identidad única en el dominio. 

En este caso, “FechaNacimiento” va a estar compuesto por los atributos de “dia”, “mes” y “anio”.  

public class FechaNacimiento {
    private int dia;
    private int mes;
    private int anio;

    public FechaNacimiento(int dia, int mes, int anio) {
        this.dia = dia;
        this.mes = mes;
        this.anio = anio;
    }

    // Getters y setters

    // Sobrescribir los métodos equals() y hashCode() para comparar objetos de valor por sus atributos

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        FechaNacimiento other = (FechaNacimiento) obj;
        return dia == other.dia &&
               mes == other.mes &&
               anio == other.anio;
    }

    @Override
    public int hashCode() {
        return Objects.hash(dia, mes, anio);
    }
}

 

Aggregate

También conocidos como agregados, agrupan un conjunto de entidades y objetos de valor valor en una unidad coherente. 

Un agregado va a estar definido por una raíz (aggregate root), que es una entidad que tiene la responsabilidad principal de mantener la coherencia y la consistencia del conjunto de objetos que forman parte del agregado. 

Solo se puede acceder y modificar el agregado a través de su raíz, lo que ayuda a preservar la integridad transaccional del conjunto de objetos y facilita la gestión del modelo de dominio.

Podemos ver un ejemplo de todo esto en un dominio que gestiona pedidos en línea. En estos casos, tendríamos un agregado que contiene atributos como id y cliente y una lista de “items” que representan los productos solicitados en el pedido. 

El agregado “Pedido” debe tener métodos que permitan operaciones como agregar o eliminar un”ItemPedido”. También puede tener otros métodos y lógica relacionados con la gestión del pedido, como aplicar descuentos, calcular el total del pedido, establecer direcciones de envío, etc. 

Por otro lado, el agregado “Pedido” debe realizar validaciones y aplicar reglas de negocio específicas para garantizar que el pedido esté en un estado válido y coherente.

public class Pedido {
    private String id;
    private Cliente cliente;
    private List<ItemPedido> items;
    // Otros atributos...

    public Pedido(String id, Cliente cliente) {
        this.id = id;
        this.cliente = cliente;
        this.items = new ArrayList<>();
    }

    // Métodos del agregado Pedido

    public void agregarItem(Producto producto, int cantidad) {
        ItemPedido nuevoItem = new ItemPedido(producto, cantidad);
        items.add(nuevoItem);
        // Lógica adicional relacionada con el pedido...
    }

    public void eliminarItem(ItemPedido item) {
        items.remove(item);
        // Lógica adicional relacionada con el pedido...
    }

    // Otros métodos y lógica relacionada con el pedido...
}



Bounded Context 

Delimitar contextos nos permite dividir un sistema en partes pequeñas y manejables.

En cada contexto hay un conjunto de conceptos y reglas de negocio por lo que, si queremos facilitar procesos de integración y mantener la consistencia del sistema es fundamental tener contextos bien delimitados que además se puedan comunicar entre sí.

En un sistema de comercio podríamos tener un contexto de “Ventas” y otro contexto de “Inventario”.

En “Ventas”,el enfoque es gestionar y procesar pedidos de clientes, realizar pagos, generar facturas y gestionar el ciclo de vida de los pedidos. Este contexto tendría entidades como “Cliente”, “Pedido”, “Producto” y “Factura” y el lenguaje de programación que empleemos será el que mejor se adapte a esos conceptos así como a reglas de negocio relacionadas con las ventas.

Por otro lado, en el contexto “Inventario” el enfoque va a ser administrar y controlar el stock de productos, realizar seguimiento a la disponibilidad y gestionar el reabastecimiento. Al igual que con el ejemplo anterior, en este contexto vamos a tener un lengua que se adapte mejor a este contexto y  entidades específicas que en este caso podrían ser "Producto", "Stock", "Proveedor" y "Orden de compra". 

¿Con qué retos me voy a encontrar si uso una arquitectura de microservicios y cómo solucionarlos?

Si bien los microservicios son una buena estrategia para desarrollar sistemas grandes, esto conlleva distintos retos que tendrán que ser sorteados de diferentes maneras. Algunos de estos retos son los siguientes:

  • Complejidad en la gestión de los microservicios.
  • Gestión de la comunicación entre microservicios.
  • Despliegue y escalabilidad.
  • Monitoreo y resolución de errores.
  • Gestión de transacciones distribuidas.
A continuación, veremos algunos patrones que nos pueden ayudar a enfrentar estos retos .

Patrones de tolerancia a fallos (resiliencia):

Cuando hablamos de sistemas, la resiliencia es  la capacidad para resistir y recuperarse de fallos. Para alcanzarla, es necesario implementar estrategias y mecanismos que nos permitan manejar situaciones de fallo, como la tolerancia a fallos y la capacidad de respuesta ante condiciones adversas.

Entre  los patrones más utilizados para mejorar la resiliencia y la estabilidad de las aplicaciones.

  • Circuit Breaker: Evita que una aplicación intente de manera reiterada una operación que con probabilidad vaya a fallar, permitiendo que esta continúe con su ejecución sin malgastar recursos mientras el problema no se resuelva
  • Retry: Este patrón permite que una aplicación maneje fallas transitorias cuando intente conectarse a un servicio o recurso de red, reintentando de manera transparente una operación fallida. Esto puede mejorar la estabilidad de la aplicación.
  • Rate Limiter: Nos ayuda a hacer que nuestros servicios estén altamente disponibles simplemente limitando la cantidad de llamadas que podemos hacer/procesar en una ventana específica. En otras palabras, nos ayuda a controlar el rendimiento. 

Actualmente una de las librerías más usadas es Resilience4J (consulta aquí la documentación oficial).

Patrones para asegurar la consistencia de la información:

Dado que estamos trabajando con distintos microservicios, donde cada uno es dueño de su propia información y base de datos, a menudo nos encontramos en la necesidad de lograr que nuestros microservicios se comuniquen entre ellos.

Sin embargo, si en algún punto esta comunicación falla, ¿cómo podemos detectar si algo salió mal y revertir los cambios en los microservicios que no fallaron? Para este tipo de situaciones utilizamos el siguiente patrón: 

Patrón Saga:  

Se utiliza  para garantizar la consistencia en operaciones que involucran múltiples pasos y servicios.

En lugar de utilizar transacciones globales, el patrón Saga divide la operación en pasos individuales y cada paso es representado por una "compensación" que puede deshacer las acciones realizadas en caso de fallo.

Este patrón puede tener dos enfoques diferentes:

 Orquestación: En este enfoque los participantes son gestionados desde un único punto: el orquestador de la saga. Mediante una primera operación orquesta las llamadas secuenciales al conjunto de participantes para llevar a cabo la transacción de negocio distribuida.


Coreografía: Los participantes reaccionan de forma autónoma. Cada servicio participa en la transacción distribuida de forma individual. Siendo responsable de gestionar su propia transacción local según la operación resultante de otro participante: continuar la saga o ejecutar la transacción de compensación (deshaciendo cambios ya realizados) desencadenando los eventos de compensación.

Conclusiones

Como decíamos al comienzo de este artículo, utilizar una arquitectura de microservicios es fundamental para crear activos digitales modulares capaces de evolucinar con las necesidades del negocio y de los usuarios finales. 

Si quieres trabajar en un empresa donde podrás fortalecer tus habilidades en  Java y arquitectura de microservicios, en compañía de  una comunidad de expertos que te ayudan a llegar al siguiente nivel, explora nuestras vacantes: 

Nueva llamada a la acción

 

Suscríbete al
Blog Pragma

Recibirás cada mes nuestra selección de contenido en Transformación digital.

Imagen form