Capítulo 1. Microservicios suficientes

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Bueno, eso se intensificó rápidamente, ¡se nos fue de las manos enseguida!

Ron Burgundy, Anchorman

Antes de sumergirnos en cómo trabajar con microservicios, es importante que tengamos una comprensión común y compartida sobre lo que son las arquitecturas de microservicios. Me gustaría abordar algunos conceptos erróneos comunes que veo habitualmente, así como matices que a menudo se pasan por alto. Necesitarás esta firme base de conocimientos para sacar el máximo partido a lo que sigue en el resto del libro. Como tal, este capítulo proporcionará una explicación de las arquitecturas de microservicios, examinará brevemente cómo se desarrollaron los microservicios (lo que significa, naturalmente, echar un vistazo a los monolitos), y examinará algunas de las ventajas y desafíos de trabajar con microservicios.

¿Qué son los microservicios?

Los microservicios son servicios que se pueden implementar de forma independiente, modelados en torno a un dominio empresarial. Se comunican entre sí a través de redes, y como opción de arquitectura ofrecen muchas opciones para resolver los problemas a los que te puedes enfrentar. De ello se deduce que una arquitectura de microservicios se basa en múltiples microservicios que colaboran entre sí.

Son un tipo de arquitectura orientada a servicios (SOA), aunque con opiniones sobre cómo deben trazarse los límites de los servicios, y que la capacidad de implementación independiente es clave. Los microservicios también tienen la ventaja de ser agnósticos respecto a la tecnología.

Desde un punto de vista tecnológico, los microservicios exponen las capacidades empresariales que encapsulan a través de uno o varios puntos finales de red. Los microservicios se comunican entre sí a través de estas redes, lo que los convierte en una forma de sistema distribuido. También encapsulan el almacenamiento y la recuperación de datos, exponiéndolos a través de interfaces bien definidas. Por tanto, las bases de datos se ocultan dentro del límite del servicio.

Hay mucho que desentrañar en todo esto, así que profundicemos un poco más en algunas de estas ideas.

Implementación independiente

La desplegabilidad independiente es la idea de que podemos hacer un cambio en un microservicio y desplegarlo en un entorno de producción sin tener que utilizar ningún otro servicio. Y lo que es más importante, no se trata sólo de que podamos hacerlo, sino de que así es como gestionas las implementaciones en tu sistema. Es una disciplina que practicas para la mayor parte de tus lanzamientos. Se trata de una idea sencilla que, sin embargo, es compleja en su ejecución.

Consejo

Si sólo sacas una cosa de este libro, debería ser ésta: asegúrate de que adoptas el concepto de capacidad de implementación independiente de tus microservicios. Acostúmbrate a introducir cambios en un único microservicio en producción sin tener que implementar nada más. De esto se derivarán muchas cosas buenas.

Para garantizar una implementación independiente, tenemos que asegurarnos de que nuestros servicios están débilmente acoplados; enotras palabras, tenemos que poder cambiar un servicio sin tener que cambiar nada más. Esto significa que necesitamos contratos explícitos, bien definidos y estables entre los servicios. Algunas opciones de implementación lo dificultan: compartir bases de datos, por ejemplo, es especialmente problemático. El deseo de servicios poco acoplados con interfaces estables guía nuestra forma de pensar sobre cómo encontrar los límites de los servicios en primer lugar.

Modelado en torno a un dominio empresarial

Hacer un cambio a través de la frontera de un proceso es caro. Si tienes que hacer un cambio en dos servicios para desplegar una función, y orquestar la implementación de esos dos cambios, eso lleva más trabajo que hacer el mismo cambio dentro de un solo servicio (o, para el caso, de un monolito). Por lo tanto, queremos encontrar formas de garantizar que los cambios entre servicios se realicen con la menor frecuencia posible.

Siguiendo el mismo enfoque que utilicé en Building Microservices, este libro utiliza un dominio y una empresa falsos para ilustrar ciertos conceptos cuando no es posible compartir historias del mundo real. La empresa en cuestión es Music Corp, una gran organización multinacional que de algún modo sigue en activo, a pesar de centrarse casi por completo en la venta de CDs.

Hemos decidido trasladar a Music Corp al siglo XXI, y para ello estamos evaluando la arquitectura actual del sistema. En la Figura 1-1, vemos una sencilla arquitectura de tres niveles. Tenemos una interfaz de usuario basada en web, una capa de lógica empresarial en forma de backend monolítico y almacenamiento de datos en una base de datos tradicional. Estas capas, como es habitual, pertenecen a equipos diferentes.

MusicCorp's systems as a traditional three-tiered architecture
Figura 1-1. Los sistemas de Music Corp como una arquitectura tradicional de tres niveles

Queremos hacer un cambio sencillo en nuestra funcionalidad: queremos permitir que nuestros clientes especifiquen su género musical favorito. Este cambio requiere que cambiemos la interfaz de usuario para mostrar la IU de elección de género, el servicio backend para permitir que el género aparezca en la IU y que se cambie el valor, y la base de datos para aceptar este cambio. Estos cambios tendrán que ser gestionados por cada equipo, como se indica en la Figura 1-2, y esos cambios tendrán que implementarse en el orden correcto.

Making a change across all three tiers is more involved
Figura 1-2. Realizar un cambio en los tres niveles es más complicado

Ahora bien, esta arquitectura no es mala. Toda arquitectura acaba optimizándose en torno a algún conjunto de objetivos. La arquitectura de tres niveles es tan común en parte porque es universal: todo el mundo ha oído hablar de ella. Así que elegir una arquitectura común que hayas visto en otros sitios suele ser una de las razones por las que seguimos viendo este patrón. Pero creo que la mayor razón por la que vemos esta arquitectura una y otra vez es porque se basa en cómo organizamos nuestros equipos.

La ahora famosa ley de Conway dice

Cualquier organización que diseñe un sistema... producirá inevitablemente un diseño cuya estructura sea una copia de la estructura de comunicación de la organización.

Melvin Conway, ¿Cómo inventan los comités?

La arquitectura de tres niveles es un buen ejemplo de ello. En el pasado, la forma principal en que las organizaciones de TI agrupaban a las personas era en función de su competencia básica: los administradores de bases de datos estaban en un equipo con otros administradores de bases de datos; los desarrolladores de Java estaban en un equipo con otros desarrolladores de Java; y los desarrolladores frontales (que hoy en día saben cosas exóticas como JavaScript y desarrollo de aplicaciones móviles nativas) estaban en otro equipo. Agrupamos a las personas en función de su competencia básica, de modo que creamos activos informáticos que pueden alinearse con esos equipos.

Eso explica por qué esta arquitectura es tan común. No es mala; simplemente está optimizada en torno a un conjunto de fuerzas: cómo agrupábamos tradicionalmente a las personas, en torno a la familiaridad. Pero las fuerzas han cambiado. Nuestras aspiraciones en torno a nuestro software han cambiado. Ahora agrupamos a las personas en equipos polivalentes, para reducir las transferencias y los silos. Queremos distribuir el software mucho más rápido que antes. Esto nos lleva a tomar decisiones diferentes sobre cómo organizamos nuestros equipos y, por tanto, sobre cómo dividimos nuestros sistemas.

Los cambios en la funcionalidad se refieren principalmente a cambios en la funcionalidad empresarial. Pero en la Figura 1-1 nuestra funcionalidad empresarial está, en efecto, repartida entre los tres niveles, lo que aumenta la probabilidad de que un cambio en la funcionalidad atraviese las capas. Se trata de una arquitectura en la que tenemos una alta cohesión de la tecnología relacionada, pero una baja cohesión de la funcionalidad empresarial. Si queremos que sea más fácil hacer cambios, tenemos que cambiar la forma en que agrupamos el código: elegimos la cohesión de la funcionalidad empresarial, en lugar de la tecnología. Cada servicio puede o no acabar conteniendo una mezcla de estas tres capas, pero eso es una cuestión de implementación local del servicio.

Comparemos esto con una posible arquitectura alternativa ilustrada en la Figura 1-3. Tenemos un servicio Cliente dedicado, que expone una interfaz de usuario para que los clientes puedan actualizar su información, y el estado del cliente también se almacena dentro de este servicio. La elección de un género favorito se asocia a un cliente determinado, por lo que este cambio es mucho más localizado. En la Figura 1-3 también vemos que la lista de géneros disponibles se obtiene de un servicio de Catálogo, algo que probablemente ya exista. También vemos un nuevo servicio de Recomendación que accede a la información de nuestro género favorito, algo que podría aparecer fácilmente en una versión posterior.

A dedicated Customer service may make it much easier to record the favorite musical genre of a customer
Figura 1-3. Un servicio de atención al cliente dedicado puede facilitar mucho la grabación del género musical favorito de un cliente

En tal situación, nuestro servicio Cliente encapsula una pequeña porción de cada uno de los tres niveles -tiene un poco de interfaz de usuario, un poco de lógica de aplicación y un poco de almacenamiento de datos-, pero todas estas capas están encapsuladas en el servicio único.

Nuestro dominio empresarial se convierte en la fuerza principal que impulsa la arquitectura de nuestro sistema, con lo que esperamos que sea más fácil hacer cambios y que nos resulte más sencillo organizar nuestros equipos en torno a nuestro dominio empresarial. Esto es tan importante que, antes de terminar este capítulo, volveremos sobre el concepto de modelar el software en torno a un dominio, para poder compartir algunas ideas sobre el diseño impulsado por el dominio que dan forma a cómo pensamos en nuestra arquitectura de microservicios.

Poseer sus propios datos

Una de las cosas que más le cuesta a la gente es la idea de que los microservicios no deben compartir bases de datos. Si un servicio quiere acceder a los datos que posee otro servicio, debe ir y pedir a ese servicio los datos que necesita. Esto da al servicio la capacidad de decidir qué se comparte y qué se oculta. También permite al servicio pasar de los detalles de implementación internos, que pueden cambiar por diversas razones arbitrarias, a un contrato público más estable, garantizando interfaces de servicio estables. Disponer de interfaces estables entre los servicios es esencial si queremos una capacidad de implementación independiente: si la interfaz que expone un servicio cambia continuamente, esto tendrá un efecto dominó que hará que otros servicios también tengan que cambiar.

Consejo

No compartas bases de datos, a menos que realmente tengas que hacerlo. E incluso entonces haz todo lo posible por evitarlo. En mi opinión, es una de las peores cosas que puedes hacer si intentas conseguir una capacidad de implementación independiente.

Como hemos comentado en la sección anterior, queremos pensar en nuestros servicios como rebanadas de funcionalidad empresarial de extremo a extremo, que cuando proceda encapsulen la interfaz de usuario, la lógica de la aplicación y el almacenamiento de datos. Esto se debe a que queremos reducir el esfuerzo necesario para cambiar la funcionalidad relacionada con el negocio. La encapsulación de datos y comportamiento de este modo nos proporciona una gran cohesión de la funcionalidad empresarial. Al ocultar la base de datos que respalda nuestro servicio, también nos aseguramos de reducir el acoplamiento. Volveremos sobre el acoplamiento y la cohesión dentro de un momento.

Esto puede ser difícil de entender, sobre todo cuando tienes un sistema monolítico con una base de datos gigante con la que tienes que lidiar. Por suerte, el Capítulo 4 está enteramente dedicado a alejarse de las bases de datos monolíticas.

¿Qué ventajas pueden aportar los microservicios?

Las ventajas de los microservicios son muchas y variadas. La naturaleza independiente de las Implementaciones abre nuevos modelos para mejorar la escala y robustez de los sistemas, y te permite mezclar y combinar tecnologías. Como se puede trabajar en los servicios en paralelo, puedes poner a más desarrolladores a trabajar en un problema sin que se estorben unos a otros. También puede ser más fácil para esos desarrolladores comprender su parte del sistema, ya que pueden centrar su atención en una sola parte del mismo. El aislamiento de procesos también nos permite variar las elecciones tecnológicas que hacemos, quizás mezclando distintos lenguajes de programación, estilos de programación, plataformas de implementación o bases de datos para encontrar la combinación adecuada.

Quizás, sobre todo, las arquitecturas de microservicios te dan flexibilidad. Abren muchas más opciones sobre cómo puedes resolver problemas en el futuro.

Sin embargo, es importante señalar que ninguna de estas ventajas es gratuita. Hay muchas formas de enfocar la descomposición del sistema, y fundamentalmente lo que intentas conseguir impulsará esta descomposición en distintas direcciones. Por tanto, es importante comprender lo que intentas obtener de tu arquitectura de microservicios.

¿Qué problemas crean?

La arquitectura orientada a servicios se puso de moda en parte porque los ordenadores eran más baratos, así que teníamos más. En lugar de implementar sistemas en un único ordenador central gigante, tenía más sentido utilizar varias máquinas más baratas. La arquitectura orientada a servicios fue un intento de encontrar la mejor manera de crear aplicaciones que abarcaran varias máquinas. Uno de los principales retos de todo esto es la forma en que estos ordenadores se comunican entre sí: las redes.

La comunicación entre ordenadores a través de redes no es instantánea (al parecer, esto tiene algo que ver con la física). Esto significa que tenemos que preocuparnos por las latencias y, en concreto, por las latencias que superan con creces las latencias que vemos con las operaciones locales, dentro del proceso. Las cosas empeoran cuando consideramos que estas latencias variarán, lo que puede hacer impredecible el comportamiento del sistema. Y también tenemos que tener en cuenta que las redes a veces fallan: los paquetes se pierden, los cables de red se desconectan...

Estos retos hacen que actividades que son relativamente sencillas con un monolito de un solo proceso, como las transacciones, sean mucho más difíciles. Tan difíciles, de hecho, que a medida que tu sistema crezca en complejidad, es probable que tengas que deshacerte de las transacciones, y de la seguridad que aportan, a cambio de otro tipo de técnicas (que desgraciadamente tienen compensaciones muy diferentes).

Lidiar con el hecho de que cualquier llamada de red puede fallar y fallará se convierte en un quebradero de cabeza, al igual que el hecho de que los servicios con los que puedas estar hablando podrían desconectarse por cualquier motivo o empezar a comportarse de forma extraña. Además de todo esto, también tienes que empezar a intentar averiguar cómo obtener una visión coherente de los datos en varias máquinas.

Y luego, por supuesto, tenemos que tener en cuenta una enorme cantidad de nueva tecnología favorable a los microservicios: nueva tecnología que, si se utiliza mal, puede ayudarte a cometer errores mucho más rápido y de formas más interesantes y costosas. Sinceramente, los microservicios parecen una idea terrible, excepto por todo lo bueno.

Merece la pena señalar que prácticamente todos los sistemas que categorizamos como "monolitos" son también sistemas distribuidos. Una aplicación de proceso único probablemente lee datos de una base de datos que se ejecuta en una máquina diferente, y presenta los datos en un navegador web. Hay al menos tres ordenadores en la mezcla, con comunicación entre ellos a través de redes. La diferencia es el grado de distribución de los sistemas monolíticos en comparación con las arquitecturas de microservicios. A medida que tienes más ordenadores en la mezcla, comunicándose a través de más redes, es más probable que te encuentres con los desagradables problemas asociados a los sistemas distribuidos. Estos problemas que he comentado brevemente pueden no aparecer inicialmente, pero con el tiempo, a medida que tu sistema crezca, es probable que te encuentres con la mayoría de ellos, si no con todos.

Como dijo mi viejo colega, amigo y colega experto en microservicios James Lewis: "Los microservicios te compran opciones". James estaba siendo deliberado con sus palabras: te compran opciones. Tienen un coste, y tienes que decidir si el coste merece la pena por las opciones que quieres aprovechar. Exploraremos este tema con más detalle en el Capítulo 2.

Interfaces de usuario

Con demasiada frecuencia, veo que la gente centra su trabajo de adopción de microservicios puramente en el lado del servidor, dejando la interfaz de usuario como una capa única y monolítica. Si queremos una arquitectura que nos facilite una implementación más rápida de nuevas funciones, dejar la interfaz de usuario como un bloque monolítico puede ser un gran error. Podemos, y debemos, considerar también la posibilidad de separar nuestras interfaces de usuario, algo que exploraremos en el Capítulo 3.

Tecnología

Puede ser demasiado tentador hacerse con un montón de tecnología nueva para acompañar a tu nueva y reluciente arquitectura de microservicios, pero te recomiendo encarecidamente que no caigas en esa tentación. Adoptar cualquier tecnología nueva tendrá un coste: creará algunos trastornos. Con suerte, merecerá la pena (¡si has elegido la tecnología adecuada, claro!), pero cuando adoptes por primera vez una arquitectura de microservicios, ya tienes bastante.

Averiguar cómo evolucionar y gestionar adecuadamente una arquitectura de microservicios implica enfrentarse a multitud de retos relacionados con los sistemas distribuidos, retos a los que quizá no te hayas enfrentado antes. Creo que es mucho más útil plantearse estos problemas a medida que te los vas encontrando, haciendo uso de una pila tecnológica con la que estés familiarizado, y luego considerar si cambiar tu tecnología actual puede ayudarte a abordar esos problemas a medida que los encuentres.

Como ya hemos comentado, los microservicios son fundamentalmente agnósticos en cuanto a la tecnología. Mientras tus servicios puedan comunicarse entre sí a través de una red, todo lo demás está en tus manos. Esto puede ser una gran ventaja, ya que te permite mezclar y combinar pilas tecnológicas si lo deseas.

No tienes que utilizar Kubernetes, Docker, contenedores o la nube pública. No tienes que codificar en Go o Rust o cualquier otro. De hecho, tu elección de lenguaje de programación es bastante poco importante cuando se trata de arquitecturas de microservicios, más allá de que algunos lenguajes puedan tener un ecosistema más rico de bibliotecas y marcos de apoyo. Si conoces mejor PHP, ¡empieza a construir servicios con PHP!1 Existe demasiado esnobismo técnico hacia algunas pilas tecnológicas que, por desgracia, puede rayar en el desprecio hacia las personas que trabajan con determinadas herramientas.2 ¡No seas parte del problema! Elige el enfoque que funcione para ti, y cambia las cosas para abordar los problemas cuando los veas.

Talla

"¿Qué tamaño debe tener un microservicio?" es probablemente la pregunta más habitual que recibo en. Teniendo en cuenta que la palabra "micro" está justo en el nombre, no es de extrañar. Sin embargo, cuando te adentras en lo que hace que los microservicios funcionen como tipo de arquitectura, el concepto de tamaño es en realidad una de las cosas menos interesantes.

¿Cómo se mide el tamaño? ¿Líneas de código? Eso no tiene mucho sentido para mí. Algo que podría requerir 25 líneas de código en Java posiblemente podría escribirse en 10 líneas de Clojure. Eso no quiere decir que Clojure sea mejor o peor que Java, sino que algunos lenguajes son más expresivos que otros.

Lo más cerca que creo estar de que el "tamaño" tenga algún significado en términos de microservicios es algo que dijo una vez otro colega experto en microservicios, Chris Richardson: que el objetivo de los microservicios es tener "una interfaz lo más pequeña posible". Esto concuerda con el concepto de ocultar información (del que hablaremos dentro de un momento), pero representa un intento de encontrar un significado a posteriori: cuando empezamos a hablar de estas cosas, nuestro principal objetivo, al menos al principio, era que fueran realmente fáciles de sustituir.

En última instancia, el concepto de "tamaño" es muy contextual. Habla con una persona que lleve 15 años trabajando en un sistema, y opinará que su sistema de 100.000 líneas de código es realmente fácil de entender. Pide la opinión de alguien totalmente nuevo en el proyecto, y sentirá que es demasiado grande. Del mismo modo, pregunta a una empresa que acaba de embarcarse en su transición a los microservicios, con quizás diez o menos microservicios, y obtendrás una respuesta diferente a la que obtendrías de una empresa de tamaño similar en la que los microservicios han sido la norma durante muchos años, y ahora tienen cientos.

Insto a la gente a que no se preocupe por el tamaño. Cuando estás empezando, es mucho más importante que te centres en dos cosas clave. Primero, ¿cuántos microservicios puedes manejar? A medida que tengas más servicios, la complejidad de tu sistema aumentará, y tendrás que aprender nuevas habilidades (y quizás adoptar nueva tecnología) para hacer frente a esto. Por eso soy un firme defensor de la migración incremental a una arquitectura de microservicios. En segundo lugar, ¿cómo definir los límites de los microservicios para sacarles el máximo partido, sin que todo se convierta en un lío horriblemente acoplado? Estos son temas que trataremos a lo largo del resto de este capítulo.

Y Propiedad

Con los microservicios modelados en torno a un dominio empresarial, vemos una alineación entre nuestros artefactos de TI (nuestros microservicios desplegables independientemente) y nuestro dominio empresarial. Esta idea resuena bien cuando consideramos el cambio hacia empresas tecnológicas que rompen las divisiones entre "El Negocio" y "TI". En las organizaciones tradicionales de TI, el acto de desarrollar software suele estar a cargo de una parte del negocio totalmente separada de la que realmente define los requisitos y tiene una conexión con el cliente, como muestra la Figura 1-4. Las disfunciones de este tipo de organizaciones son muchas y variadas, y probablemente no sea necesario ampliarlas aquí.

An organizational view of the traditional IT/business divide
Figura 1-4. Una visión organizativa de la división tradicional entre TI y negocio

En cambio, estamos viendo que las verdaderas organizaciones tecnológicas combinan totalmente estos silos organizativos dispares anteriores, como vemos en la Figura 1-5. Los propietarios de los productos trabajan ahora directamente como parte de los equipos de entrega, y estos equipos están alineados en torno a líneas de productos orientadas al cliente, en lugar de en torno a agrupaciones técnicas arbitrarias. En lugar de que las funciones centralizadas de TI sean la norma, la existencia de cualquier función central de TI es para apoyar a estos equipos de entrega orientados al cliente.

An example of how true Technology Companies are integrating software delivery
Figura 1-5. Un ejemplo de cómo las verdaderas empresas tecnológicas están integrando la entrega de software

Aunque no todas las organizaciones han hecho este cambio, las arquitecturas de microservicios lo facilitan mucho. Si quieres equipos de entrega alineados en torno a líneas de producto, y los servicios están alineados en torno al dominio empresarial, entonces resulta más fácil asignar claramente la propiedad a estos equipos de entrega orientados al producto. Reducir los servicios que se comparten entre varios equipos es clave para minimizar la contención en la entrega: las arquitecturas de microservicios orientadas al dominio empresarial facilitan mucho este cambio en las estructuras organizativas.

El Monolito

Hemos hablado de microservicios, pero este libro trata de pasar de monolitos a microservicios, por lo que también debemos establecer qué se entiende por monolito.

Cuando hablo de los monolitos en este libro, me refiero principalmente a una unidad de implementación. Cuando todas las funcionalidades de un sistema deben desplegarse juntas, lo consideramos un monolito. Hay al menos tres tipos de sistemas monolíticos que encajan en esta categoría: el sistema de proceso único, el monolito distribuido y los sistemas de caja negra de terceros.

El monolito de un solo proceso

El ejemplo más común que nos viene a la mente cuando hablamos de monolitos es un sistema en el que todo el código se implementa en como un único proceso, como en la Figura 1-6. Puedes tener varias instancias de este proceso por razones de robustez o escalabilidad, pero fundamentalmente todo el código está empaquetado en un único proceso. En realidad, estos sistemas de proceso único pueden ser simples sistemas distribuidos por derecho propio, ya que casi siempre acaban leyendo datos de una base de datos o almacenándolos en ella.

A single-process monolith. All code is packaged into a single process.
Figura 1-6. Un monolito de un solo proceso: todo el código se empaqueta en un único proceso

Estos monolitos de un solo proceso representan probablemente la gran mayoría de los sistemas monolíticos con los que veo a la gente luchar, y por tanto son los tipos de monolitos en los que nos centraremos la mayor parte de nuestro tiempo. A partir de ahora, cuando utilice el término "monolito", me referiré a este tipo de monolitos, a menos que diga lo contrario.

Y el monolito modular

Como subconjunto del monolito de proceso único, el monolito modular es una variación: el proceso único consta de módulos separados, cada uno de los cuales puede trabajarse de forma independiente, pero que aún deben combinarse para su implementación, como se muestra en la Figura 1-7. El concepto de dividir el software en módulos no es nada nuevo; volveremos sobre la historia de esto más adelante en este capítulo.

A modular process monolith. The code inside the process is broken into modules.
Figura 1-7. Un monolito modular: el código dentro del proceso se divide en módulos

Para muchas organizaciones, el monolito modular puede ser una opción excelente. Si los límites de los módulos están bien definidos, puede permitir un alto grado de trabajo en paralelo, pero elude los retos de la arquitectura de microservicios más distribuida, junto con problemas de implementación mucho más sencillos. Shopify es un gran ejemplo de organización que ha utilizado esta técnica como alternativa a la descomposición de microservicios, y parece funcionar realmente bien para esa empresa.4

Uno de los retos de un monolito modular es que la base de datos tiende a carecer de la descomposición que encontramos en el nivel de código, lo que conlleva importantes desafíos a los que enfrentarse si quieres tirar del monolito en el futuro. He visto a algunos equipos intentar llevar más allá la idea del monolito modular, haciendo que la base de datos se descomponga siguiendo las mismas líneas que los módulos, como se muestra en la Figura 1-8. Fundamentalmente, hacer un cambio como éste en un monolito existente puede seguir siendo muy difícil incluso si dejas el código solo: muchos de los patrones que exploraremos en el Capítulo 4 pueden ayudarte si quieres intentar hacer algo similar tú mismo.

A modular monolith with a decomposed database.
Figura 1-8. Un monolito modular con una base de datos descompuesta

El monolito distribuido

Un sistema distribuido es aquel en el que el fallo de un ordenador que ni siquiera sabías que existía puede inutilizar tu propio ordenador.5

Leslie Lamport

Un monolito distribuido es un sistema que consta de múltiples servicios, pero por la razón que sea todo el sistema tiene que desplegarse junto. Un monolito distribuido puede ajustarse perfectamente a la definición de una arquitectura orientada a servicios, pero con demasiada frecuencia no cumple las promesas de la SOA. En mi experiencia, los monolitos distribuidos tienen todas las desventajas de un sistema distribuido, y las desventajas de un monolito de proceso único, sin tener suficientes ventajas de ninguno de los dos. Encontrarme con monolitos distribuidos en mi trabajo ha influido en gran medida en mi propio interés por la arquitectura de microservicios.

Los monolitos distribuidos suelen surgir en un entorno en el que no se prestó suficiente atención a conceptos como la ocultación de información y la cohesión de la funcionalidad empresarial, lo que dio lugar a arquitecturas muy acopladas en las que los cambios se propagan a través de los límites de los servicios, y cambios aparentemente inocentes que parecen de alcance local rompen otras partes del sistema.

Sistemas de caja negra de terceros

También podemos considerar algunos programas de terceros como monolitos que quizá queramos "descomponer" como parte de un esfuerzo de migración. Podrían ser cosas como sistemas de nóminas, sistemas CRM y sistemas de RRHH. El factor común aquí es que se trata de software desarrollado por otras personas, y tú no tienes capacidad para cambiar el código. Podría tratarse de software comercial que has implementado en tu propia infraestructura, o podría ser un producto de software como servicio (SaaS) que estás utilizando. Muchas de las técnicas de descomposición que exploraremos en este libro pueden utilizarse incluso con sistemas en los que no puedes cambiar el código subyacente.

Los retos de los monolitos

El monolito, ya sea un monolito de un solo proceso o un monolito distribuido, suele ser más vulnerable a los peligros del acoplamiento; en concreto, al acoplamiento de implementación y despliegue, temas que analizaremos más adelante.

A medida que aumenta el número de personas que trabajan en el mismo lugar, se estorban mutuamente. Diferentes desarrolladores que quieren cambiar el mismo fragmento de código, diferentes equipos que quieren lanzar la funcionalidad en diferentes momentos (o retrasar las Implementaciones). Confusión sobre quién es dueño de qué y quién toma las decisiones. Multitud de estudios muestran los retos que plantea la confusión en las líneas de propiedad.6 Yo llamo a este problema contención en la entrega.

Tener un monolito no significa que te enfrentarás definitivamente a los retos de la contención en la entrega, como tampoco tener una arquitectura de microservicios significa que nunca te enfrentarás al problema. Pero una arquitectura de microservicios sí te proporciona límites más concretos en un sistema en torno a los cuales se pueden trazar líneas de propiedad, lo que te da mucha más flexibilidad en cuanto a cómo reducir este problema.

Ventajas de los monolitos

Sin embargo, el monolito de un solo proceso también tiene toda una serie de ventajas. Su topología de implementación mucho más sencilla puede evitar muchos de los escollos asociados a los sistemas distribuidos. Puede simplificar mucho los flujos de trabajo de los desarrolladores, así como el monitoreo, la resolución de problemas y actividades como las pruebas de extremo a extremo.

Los monolitos también pueden simplificar la reutilización de código dentro del propio monolito. Si queremos reutilizar código dentro de un sistema distribuido, tenemos que decidir si queremos copiar código, dividir bibliotecas o introducir la funcionalidad compartida en un servicio. Con un monolito, nuestras opciones son mucho más sencillas, y a mucha gente le gusta esa simplicidad: todo el código está ahí, así que ¡úsalo!

Por desgracia, la gente ha llegado a considerar el monolito como algo que hay que evitar, como algo inherentemente problemático. He conocido a muchas personas para las que el término monolito es sinónimo de legado. Esto es un problema. Una arquitectura monolítica es una elección, y una elección válida. Puede que no sea la elección correcta en todas las circunstancias, como tampoco lo son los microservicios, pero no deja de ser una elección. Si caemos en la trampa de denigrar sistemáticamente el monolito como opción viable para entregar nuestro software, corremos el riesgo de no hacer lo correcto ni para nosotros ni para los usuarios de nuestro software. En el Capítulo 3 exploraremos más a fondo las ventajas y desventajas de los monolitos y los microservicios, y hablaremos de algunas herramientas que te ayudarán a evaluar mejor lo que es adecuado para tu propio contexto.

Sobre Acoplamiento y Cohesión

Comprender las fuerzas de equilibrio entre acoplamiento y cohesión es importante a la hora de definir los límites de los microservicios. El acoplamiento habla de cómo cambiar una cosa requiere un cambio en otra; la cohesión habla de cómo agrupamos el código relacionado. Estos conceptos están directamente relacionados. La ley de Constantine articula bien esta relación:

Una estructura es estable si la cohesión es alta y el acoplamiento bajo.

Larry Constantine

Parece una observación sensata y útil. Si tenemos dos trozos de código estrechamente relacionados, la cohesión es baja, ya que la funcionalidad relacionada está repartida entre ambos. También tenemos un acoplamiento estrecho, ya que cuando este código relacionado cambia, ambas cosas tienen que cambiar.

Si la estructura de nuestro sistema de código cambia, será costoso abordarlo, ya que el coste del cambio a través de los límites de los servicios en los sistemas distribuidos es muy alto. Tener que hacer cambios en uno o más servicios que se puedan implementar independientemente, y quizás tener que lidiar con el impacto de los cambios de ruptura de los contratos de servicio, puede suponer un enorme lastre.

El problema del monolito es que con demasiada frecuencia es lo contrario de ambas cosas. En lugar de tender hacia la cohesión, y mantener juntas cosas que tienden a cambiar juntas, adquirimos y pegamos todo tipo de código no relacionado. Del mismo modo, el acoplamiento débil no existe realmente: si quiero hacer un cambio en una línea de código, puedo hacerlo con bastante facilidad, pero no puedo desplegar ese cambio sin afectar potencialmente a gran parte del resto del monolito, y seguramente tendré que volver a desplegar todo el sistema.

También queremos estabilidad en el sistema porque nuestro objetivo, en la medida de lo posible, es adoptar el concepto de capacidad de implementación independiente, es decir, nos gustaría poder hacer un cambio en nuestro servicio y desplegar ese servicio en producción sin tener que cambiar nada más. Para que esto funcione, necesitamos estabilidad de los servicios que consumimos, y necesitamos proporcionar un contrato estable a los servicios que nos consumen.

Dada la gran cantidad de información que hay por ahí sobre estos términos, sería una tontería por mi parte volver sobre ellos aquí, pero creo que es conveniente hacer un resumen, sobre todo para situar estas ideas en el contexto de las arquitecturas de microservicios. En última instancia, estos conceptos de cohesión y acoplamiento influyen enormemente en cómo pensamos sobre la arquitectura de microservicios. Y no es de extrañar: la cohesión y el acoplamiento son preocupaciones relacionadas con el software modular, y ¿qué es la arquitectura de microservicios sino módulos que se comunican a través de redes y pueden desplegarse independientemente?

Cohesión

Una de las definiciones más sucintas de que he oído para describir la cohesión es la siguiente: "el código que cambia junto, permanece junto". Para nuestros propósitos, ésta es una definición bastante buena. Como ya hemos comentado, estamos optimizando nuestra arquitectura de microservicios en torno a la facilidad para realizar cambios en la funcionalidad empresarial, por lo que queremos que la funcionalidad se agrupe de tal manera que podamos realizar cambios en el menor número de lugares posible.

Si quiero cambiar la forma en que se gestiona la aprobación de facturas, no quiero tener que buscar la funcionalidad que hay que cambiar en varios servicios, y luego coordinar la liberación de esos servicios recién cambiados para desplegar nuestra nueva funcionalidad. En lugar de eso, quiero asegurarme de que el cambio implique modificaciones en el menor número posible de servicios para mantener bajo el coste del cambio.

Acoplamiento

Ocultar información, como hacer dieta, es algo más fácil de describir que de hacer.

David Parnas, La historia secreta de la ocultación de información

Nos gusta la cohesión, pero desconfiamos del acoplamiento. Cuantas más cosas estén "acopladas", más tendrán que cambiar juntas. Pero hay distintos tipos de acoplamiento, y cada tipo puede requerir soluciones distintas.

Ha habido mucho arte previo en lo que respecta a la categorización de los tipos de acoplamiento, en particular el trabajo realizado por Meyer, Yourdan y Constantine. Yo presento la mía, no para decir que el trabajo realizado anteriormente sea erróneo, más que esta categorización me parece más útil a la hora de ayudar a la gente a comprender aspectos asociados al acoplamiento de sistemas distribuidos. Como tal, no pretende ser una clasificación exhaustiva de las distintas formas de acoplamiento.

Acoplamiento de aplicación

El acoplamiento de implementación suele ser la forma más perniciosa de acoplamiento que veo, pero por suerte para nosotros suele ser una de las más fáciles de reducir. Con el acoplamiento de implementación, A se acopla a B en función de cómo se implementa B: cuando cambia la implementación de B, también cambia A.

La cuestión aquí es que los detalles de implementación suelen ser una elección arbitraria de los desarrolladores. Hay muchas formas de resolver un problema; elegimos una, pero podemos cambiar de opinión. Cuando decidimos cambiar de opinión, no queremos que esto rompa a los consumidores (desplegabilidad independiente, ¿recuerdas?).

Un ejemplo clásico y habitual de acoplamiento de implementaciones es el uso compartido de una base de datos. En la Figura 1-9, nuestro servicio Pedidos contiene un registro de todos los pedidos realizados en nuestro sistema. El servicio de Recomendación sugiere a nuestros clientes registros que les gustaría comprar basándose en compras anteriores. Actualmente, el servicio de Recomendación accede directamente a estos datos desde la base de datos.

The Recommendation service directly accesses the data stored in the Order service
Figura 1-9. El servicio Recomendación accede directamente a los datos almacenados en el servicio Pedido

Las recomendaciones requieren información sobre qué pedidos se han realizado. Hasta cierto punto, se trata de un acoplamiento de dominio inevitable, al que nos referiremos dentro de un momento. Pero en esta situación concreta, estamos acoplados a una estructura de esquema específica, a un dialecto SQL y quizá incluso al contenido de las filas. Si el servicio Pedido cambia el nombre de una columna, divide la tabla Pedido del cliente o cualquier otra cosa, conceptualmente sigue conteniendo información del pedido, pero rompemos la forma en que el servicio Recomendación obtiene esta información. Una opción mejor es ocultar este detalle de implementación, como muestra la Figura 1-10: ahora el servicio de Recomendación accede a la información que necesita mediante una llamada a la API.

The Recommendation service now accesses order information via an API, hiding internal implementation detail
Figura 1-10. El servicio de Recomendación accede ahora a la información del pedido a través de una API, ocultando los detalles de la implementación interna

También podríamos hacer que el servicio de Pedidos publicara un conjunto de datos, en forma de base de datos, destinado a ser utilizado para el acceso masivo de los consumidores, tal y como vemos en la Figura 1-11. Mientras el servicio de Pedidos pueda publicar los datos como corresponde, cualquier cambio realizado dentro del servicio de Pedidos será invisible para los consumidores, ya que mantiene el contrato público. Esto también abre la oportunidad de mejorar el modelo de datos expuesto a los consumidores, ajustándolo a sus necesidades. Exploraremos patrones como éste con más detalle en los Capítulos 3 y 4.

The Recommendation service now accesses order information via an exposed DB, which is structured differently from the internal database
Figura 1-11. El servicio de Recomendación accede ahora a la información de los pedidos a través de una base de datos expuesta, que está estructurada de forma diferente a la base de datos interna

En efecto, con los dos ejemplos anteriores, estamos haciendo uso de la ocultación de información. El hecho de ocultar una base de datos tras una interfaz de servicio bien definida permite al servicio limitar el alcance de lo que se expone, y puede permitirnos cambiar cómo se representan esos datos.

Otro truco útil es utilizar el pensamiento "de fuera a dentro" a la hora de definir una interfaz de servicio: dirige la interfaz de servicio pensando primero desde el punto de vista de los consumidores del servicio, y luego resuelve cómo implementar ese contrato de servicio. El enfoque alternativo (que he observado que es demasiado común, por desgracia) es hacer lo contrario. El equipo que trabaja en el servicio toma un modelo de datos, u otro detalle de implementación interna, y luego piensa en exponerlo al mundo exterior.

Con el pensamiento "de fuera a dentro", en cambio, primero preguntas: "¿Qué necesitan los consumidores de mi servicio?". Y no me refiero a que te preguntes a ti mismo qué necesitan tus consumidores; ¡me refiero a que preguntes realmente a las personas que llamarán a tu servicio!

Consejo

Trata las interfaces de servicio que expone tu microservicio como una interfaz de usuario. Utiliza el pensamiento outside-in para dar forma al diseño de la interfaz en colaboración con las personas que llamarán a tu servicio.

Piensa en tu contrato de servicios con el mundo exterior como en una interfaz de usuario. Cuando diseñas una interfaz de usuario, preguntas a los usuarios qué quieren, e iteras sobre el diseño de ésta con tus usuarios. Deberías dar forma a tu contrato de servicios de la misma manera. Aparte del hecho de que significa que acabas con un servicio más fácil de usar para tus consumidores, también ayuda a mantener cierta separación entre el contrato externo y la implementación interna.

Acoplamiento temporal

El acoplamiento temporal es principalmente una preocupación en tiempo de ejecución que, en general, se refiere a uno de los retos clave de las llamadas síncronas en un entorno distribuido. Cuando se envía un mensaje, y cómo se gestiona ese mensaje está conectado en el tiempo, se dice que tenemos acoplamiento temporal. Eso suena un poco raro, así que veamos un ejemplo explícito en la Figura 1-12.

Three services making use of synchronous calls to perform an operation can be said to be temporally coupled
Figura 1-12. Se puede decir que tres servicios que utilizan llamadas síncronas para realizar una operación están temporalmente acoplados

Aquí vemos una llamada HTTP síncrona realizada desde nuestro servicio de Almacén a un servicio de Pedido descendente para obtener la información necesaria sobre un pedido. Para satisfacer la solicitud, el servicio de Pedidos tiene que obtener a su vez información del servicio de Clientes, también mediante una llamada HTTP sincrónica. Para que esta operación global se complete, los servicios de Almacén, Pedido y Cliente tienen que estar activos y localizables. Están acoplados temporalmente.

Podríamos reducir este problema de varias formas. Podríamos considerar el uso de caché: si el servicio Pedido almacenara en caché la información que necesita del servicio Cliente, el servicio Pedido podría evitar el acoplamiento temporal en el servicio descendente en algunos casos. También podríamos considerar el uso de un transporte asíncrono para enviar las peticiones, quizás utilizando algo como un corredor de mensajes. Esto permitiría enviar un mensaje a un servicio descendente, y que ese mensaje se gestionara después de que el servicio descendente estuviera disponible.

Una exploración completa de los tipos de comunicación entre servicios queda fuera del alcance de este libro, pero se trata con más detalle en el capítulo 4 de Construir microservicios.

Acoplamiento de Implementaciones

Considera un proceso único, que consta de varios módulos enlazados estáticamente. Se realiza un cambio en una sola línea de código de uno de los módulos, y queremos desplegar ese cambio. Para ello, tenemos que desplegar todo el monolito, incluso los módulos que no han cambiado. Todo debe desplegarse a la vez, por lo que tenemos un acoplamiento de implementación.

El acoplamiento de la implementación puede ser obligatorio, como en el ejemplo de nuestro proceso estáticamente vinculado, pero también puede ser una cuestión de elección, impulsada por prácticas como el tren de lanzamientos. Con un tren de lanzamientos, se elaboran calendarios de lanzamientos planificados de antemano, normalmente con un calendario repetitivo. Cuando llega el momento de la publicación, se implementan todos los cambios realizados desde el último tren de publicación. Para algunas personas, el tren de versiones puede ser una técnica útil, pero yo prefiero verlo como un paso transitorio hacia las técnicas adecuadas de liberación bajo demanda, en lugar de verlo como un objetivo final. Incluso he trabajado en organizaciones que desplegaban todos los servicios de un sistema a la vez como parte de estos procesos de tren de liberación, sin pensar en si era necesario cambiar esos servicios.

Implementar algo conlleva un riesgo. Hay muchas formas de reducir el riesgo de la implementación, y una de ellas es cambiar sólo lo que haya que cambiar. Si podemos reducir el acoplamiento de la implementación, tal vez descomponiendo procesos más grandes en microservicios que se puedan implementar independientemente, podemos reducir el riesgo de cada implementación reduciendo el alcance de la implementación.

Los lanzamientos más pequeños conllevan menos riesgos. Hay menos cosas que puedan salir mal. Si algo sale mal, averiguar qué ha fallado y cómo solucionarlo es más fácil porque hemos cambiado menos. Encontrar formas de reducir el tamaño de la versión es fundamental para la entrega continua, que defiende la importancia de la retroalimentación rápida y los métodos de publicación bajo demanda.9 Cuanto menor sea el alcance del lanzamiento, más fácil y seguro será desplegarlo, y más rápido obtendremos la retroalimentación. Mi propio interés en los microservicios proviene de un enfoque anterior en la entrega continua: buscaba arquitecturas que facilitaran la adopción de la entrega continua.

Reducir el acoplamiento de la implementación no requiere microservicios, por supuesto. Los tiempos de ejecución como Erlang permiten la implementación en caliente de nuevas versiones de módulos en un proceso en ejecución. Con el tiempo, quizá seamos más los que tengamos acceso a tales capacidades en las pilas tecnológicas que utilizamos a diario.10

Acoplamiento de dominios

Fundamentalmente, en un sistema que consta de múltiples servicios independientes, tiene que haber alguna interacción entre los participantes. En una arquitectura de microservicios, el acoplamiento de dominio es el resultado: las interacciones entre servicios modelan las interacciones en nuestro dominio real. Si quieres hacer un pedido, necesitas saber qué artículos había en la cesta de la compra de un cliente. Si quieres enviar un producto, necesitas saber dónde lo envías. En nuestra arquitectura de microservicios, por definición esta información puede estar contenida en diferentes servicios.

Para dar un ejemplo concreto, considera Music Corp. Tenemos un almacén que guarda mercancías. Cuando los clientes hacen pedidos de CD, la gente que trabaja en el almacén tiene que saber qué artículos hay que recoger y empaquetar, y adónde hay que enviar el paquete. Por tanto, hay que compartir información sobre el pedido con las personas que trabajan en el almacén.

La Figura 1-13 muestra un ejemplo de esto: un servicio de Procesamiento de Pedidos envía todos los detalles del pedido al servicio Almacén, que a su vez desencadena el empaquetado del artículo. Como parte de esta operación, el servicio Almacén utiliza el identificador del cliente para obtener información sobre él del servicio independiente Cliente, de modo que sepamos cómo notificarle el envío del pedido.

En esta situación, estamos compartiendo todo el pedido con el almacén, lo que puede no tener sentido: el almacén sólo necesita información sobre qué empaquetar y dónde enviarlo. No necesitan saber cuánto cuesta el artículo (si necesitan incluir una factura con el paquete, ésta podría pasarse como un PDF pre-renderizado). También tendríamos problemas con el hecho de que la información a la que tenemos que controlar el acceso se compartiera demasiado ampliamente: si compartiéramos el pedido completo, podríamos acabar exponiendo los datos de la tarjeta de crédito a servicios que no los necesitan, por ejemplo.

An Order is sent to the Warehouse to allow packaging to commence
Figura 1-13. Se envía un pedido al almacén para que comience el embalaje

Así que, en su lugar, podríamos idear un nuevo concepto de dominio de una Instrucción de Recogida que contenga sólo la información que necesita el servicio de Almacén, como vemos en la Figura 1-14. Éste es otro ejemplo de ocultación de información.

Using a Pick instruction to reduce how much information we send to the Warehouse
Figura 1-14. Utilizar una Instrucción de Recogida para reducir la cantidad de información que enviamos al servicio de Almacén

Podríamos reducir aún más el acoplamiento eliminando la necesidad de que el servicio Almacén conozca siquiera a un cliente si así lo quisiéramos; en su lugar, podríamos proporcionar todos los detalles apropiados a través de la Instrucción de Recogida, como muestra la Figura 1-15.

Putting more information into the Pick Instruction can avoid the need for a call to the Customer service
Figura 1-15. Poner más información en la Instrucción de Recogida puede evitar la necesidad de una llamada al servicio de Atención al Cliente

Para que este enfoque funcione, probablemente signifique que en algún momento el Procesamiento de Pedidos tenga que acceder al servicio Cliente para poder generar esta Instrucción de Recogida en primer lugar, pero es probable que el Procesamiento de Pedidos necesite acceder a la información del cliente por otras razones de todos modos, así que es poco probable que esto suponga un gran problema. Este proceso de "enviar" una Instrucción de Recogida implica que se realiza una llamada a la API desde el Procesamiento de Pedidos al servicio de Almacén.

Una alternativa podría ser que el Procesamiento de Pedidos emitiera algún tipo de evento que consumiera el Almacén, en la Figura 1-16. Al emitir un evento que consume el Almacén, invertimos las dependencias. Pasamos de que el Procesamiento de Pedidos dependa del servicio Almacén para poder garantizar el envío de un pedido, a que el Almacén escuche los eventos del servicio Procesamiento de Pedidos. Ambos enfoques tienen sus ventajas, y el que yo eligiera probablemente dependería de una comprensión más amplia de las interacciones entre la lógica del Procesamiento de Pedidos y la funcionalidad encapsulada en el servicio de Almacén; eso es algo en lo que puede ayudar algo de modelado de dominio, un tema que exploraremos a continuación.

Firing an event which the Warehouse service can receive, containing just enough information for the order to be packaged and sent
Figura 1-16. Lanzar un evento que pueda recibir el servicio Almacén, que contenga la información suficiente para que el pedido se empaquete y envíe

Fundamentalmente, se necesita cierta información sobre un pedido para que el servicio Almacén realice algún trabajo. No podemos evitar ese nivel de acoplamiento de dominio. Pero si pensamos detenidamente qué y cómo compartimos esos conceptos, podemos aspirar a reducir el nivel de acoplamiento utilizado.

Diseño orientado al dominio suficiente

Como ya hemos comentado, modelar nuestros servicios en torno a un dominio empresarial tiene importantes ventajas para nuestra arquitectura de microservicios. La cuestión es cómo crear ese modelo, y aquí es donde entra en juego el diseño orientado al dominio (DDD).

El deseo de que nuestros programas representen mejor el mundo real en el que operarán los propios programas no es una idea nueva. Los lenguajes de programación orientados a objetos como Simula se desarrollaron para permitirnos modelar dominios reales. Pero hace falta algo más que las capacidades del lenguaje de programación para que esta idea tome realmente forma.

El Diseño Orientado al Dominio de Eric Evans11 presentó una serie de ideas importantes que nos ayudaron a representar mejor el dominio del problema en nuestros programas. Una exploración completa de estas ideas está fuera del alcance de este libro, pero proporcionaré una breve visión general de las ideas más importantes implicadas en la consideración de las arquitecturas de microservicios.

Agregado

En DDD, un agregado es un concepto algo confuso, con muchas definiciones diferentes por ahí. ¿Es sólo una colección arbitraria de objetos? ¿Es la unidad más pequeña que debo sacar de una base de datos? El modelo que siempre me ha funcionado es considerar primero un agregado como una representación de un concepto de dominio real: piensa en algo como un Pedido, una Factura, un Artículo de Stock, etc. Los agregados suelen tener un ciclo de vida a su alrededor, lo que permite implementarlos como una máquina de estados. Queremos tratar los agregados como unidades autocontenidas; queremos asegurarnos de que el código que maneja las transiciones de estado de un agregado se agrupa, junto con el propio estado.

Al pensar en agregados y microservicios, un único microservicio se encargará del ciclo de vida y del almacenamiento de datos de uno o varios tipos diferentes de agregados. Si la funcionalidad de otro servicio quiere cambiar uno de estos agregados, necesita o bien solicitar directamente un cambio en ese agregado, o bien hacer que el propio agregado reaccione a otras cosas en el sistema para iniciar sus propias transiciones de estado -ejemplos que vemos ilustrados en la Figura 1-17.

Lo que hay que entender aquí es que si una parte externa solicita una transición de estado en un agregado, éste puede decir que no. Lo ideal es que implementes tus agregados de forma que las transiciones de estado ilegales sean imposibles.

Los agregados pueden tener relaciones con otros agregados. En la Figura 1-18, tenemos un agregado Cliente, que está asociado a uno o varios Pedidos. Hemos decidido modelar Cliente y Pedido como agregados separados, que podrían ser gestionados por servicios distintos.

Different ways in which our Payment service may trigger a Paid transition in our Invoice aggregate
Figura 1-17. Diferentes formas en las que nuestro servicio de Pago puede desencadenar una transición Pagado en nuestro agregado Factura
One Customer aggregate may be associated with one or more Order aggregates
Figura 1-18. Un agregado Cliente puede estar asociado a uno o varios agregados Pedido

Hay muchas formas de dividir un sistema en agregados, y algunas opciones son muy subjetivas. Es posible que, por razones de rendimiento o facilidad de implementación, decidas remodelar los agregados con el tiempo. Para empezar, sin embargo, considero que las preocupaciones de implementación son secundarias, dejando inicialmente que el modelo mental de los usuarios del sistema sea la luz que me guíe en el diseño inicial hasta que otros factores entren en juego. En el Capítulo 2, presentaré el Event Storming como un ejercicio de colaboración para ayudar a dar forma a estos modelos de dominio con la ayuda de tus colegas no desarrolladores.

Contexto limitado

Un contexto delimitado suele representar un límite organizativo mayor dentro de una organización. Dentro del ámbito de ese límite, deben llevarse a cabo responsabilidades explícitas. Todo esto es un poco confuso, así que veamos otro ejemplo concreto.

En Music Corp, nuestro almacén es un hervidero de actividad: gestión de los pedidos que se envían (y de alguna que otra devolución), recepción de nuevas existencias, carreras de carretillas elevadoras, etcétera. Por otra parte, el departamento financiero es quizás menos amante de la diversión, pero sigue teniendo una función importante dentro de nuestra organización, gestionando las nóminas, pagando los envíos y cosas por el estilo.

Los contextos limitados ocultan los detalles de implementación. Hay cuestiones internas: por ejemplo, los tipos de carretillas elevadoras que se utilizan no interesan a nadie más que a la gente del almacén. Estas cuestiones internas deben ocultarse al mundo exterior: no tienen por qué saberlo, ni debe importarles.

Desde el punto de vista de la implementación, los contextos delimitados contienen uno o varios agregados. Algunos agregados pueden estar expuestos fuera del contexto delimitado; otros pueden estar ocultos internamente. Al igual que los agregados, los contextos delimitados pueden tener relaciones con otros contextos delimitados; cuando se asignan a servicios, estas dependencias se convierten en dependencias entre servicios.

Asignación de agregados y contextos limitados a microservicios

Tanto el agregado como el contexto delimitado nos proporcionan unidades de cohesión con interfaces bien definidas con el sistema más amplio. El agregado es una máquina de estados autónoma que se centra en un único concepto de dominio de nuestro sistema, y el contexto delimitado representa una colección de agregados asociados, de nuevo con una interfaz explícita con el mundo más amplio.

Por tanto, ambos pueden funcionar bien como límites de servicio. Al empezar, como ya he mencionado, creo que te conviene reducir el número de servicios con los que trabajas. En consecuencia, creo que probablemente deberías centrarte en servicios que abarquen contextos delimitados enteros. A medida que te familiarices con ellos y decidas dividirlos en servicios más pequeños, intenta dividirlos en torno a límites agregados.

Un truco aquí es que, aunque decidas dividir más adelante un servicio que modela todo un contexto delimitado en servicios más pequeños, puedes ocultar esta decisión al mundo exterior, tal vez presentando una API de grano más grueso a los consumidores. Podría decirse que la decisión de descomponer un servicio en partes más pequeñas es una decisión de implementación, ¡así que mejor ocultarla si podemos!

Otras lecturas

Una exploración a fondo del diseño dirigido por dominios es una actividad que merece la pena, pero queda fuera del alcance de este libro. Si quieres profundizar en ello, te sugiero que leas el original Domain Driven Design de Eric Evans o el Domain-Driven Design Distilled de Vaughn Vernon.12

Resumen

Como hemos visto en este capítulo, los microservicios son servicios que se pueden implementar de forma independiente, modelados en torno a un dominio empresarial. Se comunican entre sí a través de redes. Utilizamos los principios de la ocultación de información junto con el diseño orientado al dominio para crear servicios con límites estables en los que sea más fácil trabajar de forma independiente, y hacemos lo que podemos para reducir las múltiples formas de acoplamiento.

También hemos repasado brevemente la historia de su origen, e incluso hemos encontrado tiempo para examinar una pequeña parte de la enorme cantidad de trabajo previo en el que se basan. También examinamos brevemente algunos de los retos asociados a las arquitecturas de microservicios. Exploraré este tema con más detalle en el próximo capítulo, donde también hablaré de cómo planificar la transición a una arquitectura de microservicios, además de proporcionar orientación para ayudarte a decidir si son adecuadas para ti en primer lugar.

1 Para saber más sobre este tema, te recomiendo PHP Web Services, de Lorna Jane Mitchell (O'Reilly).

2 Tras leer la entrada del blog "Cultura del desprecio" de Aurynn Shaw, reconocí que en el pasado he sido culpable de mostrar cierto grado de desprecio hacia distintas tecnologías y, por extensión, hacia las comunidades que las rodean.

3 No recuerdo la primera vez que escribimos realmente el término, pero recuerdo vívidamente mi insistencia, frente a toda lógica gramatical, en que el término no debía llevar guión. En retrospectiva, era una postura difícil de justificar, a la que sin embargo me atuve. Mantengo mi decisión poco razonable, pero finalmente victoriosa.

4 Para una visión general del pensamiento de Shopify detrás del uso de un monolito modular en lugar de microservicios, la charla de Kirsten Westeinde en YouTube tiene algunas ideas útiles.

5 Mensaje de correo electrónico enviado a un tablón de anuncios del DEC SRC a las 12:23:29 PDT del 28 de mayo de 1987 (para más información , consulta https://www.microsoft.com/en-us/research/publication/distribution/ ).

6 Microsoft Research ha realizado estudios en este espacio, y los recomiendo todos. Como punto de partida, sugiero "¡No toques mi código! Examining the Effects of Ownership on Software Quality" de Christian Bird et al.

7 Aunque a menudo se cita como fuente el conocido artículo de Parnas de 1972 "On the Criteria to be Used in Decomposing Systems into Modules", él compartió por primera vez este concepto en "Information Distributions Aspects of Design Methodology", Proceedings of IFIP Congress '71, 1971.

8 Véase Parnas, David, "La historia secreta de la ocultación de información". Publicado en Software Pioneers, eds. M. Broy y E. Denert (Berlín Heidelberg: Springer, 2002).

9 Véase Jez Humble y David Farley, Continuous Delivery: Reliable Software Releases through Build, Test, and Implementación Automation (Upper Saddle River: Addison Wesley, 2010) para más detalles.

10 La 10ª regla de Greenspun dice: "Cualquier programa C o Fortran suficientemente complicado contiene una implementación ad hoc, especificada informalmente, plagada de errores y lenta de la mitad de Common Lisp". Esto se ha transformado en el chiste más reciente: "Toda arquitectura de microservicios contiene una reimplementación medio rota de Erlang". Creo que hay mucho de cierto en esto.

11 Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (Boston: Addison-Wesley, 2004).

12 Véase Vaughn Vernon, Domain-Driven Design Distilled (Boston: Addison-Wesley, 2014).

Get De monolito a microservicio now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.