Arquitectura basada en microservicios: dividir un sistema para hacerlo evolucionar
Durante mucho tiempo, construir una aplicación significaba reunir buena parte de su funcionalidad en un único programa. Esa forma de organizar el software puede funcionar muy bien, especialmente cuando el sistema es pequeño o cuando el equipo necesita avanzar rápidamente. El problema aparece cuando la aplicación crece: cada nueva funcionalidad empieza a relacionarse con muchas otras, los cambios afectan a zonas que aparentemente no deberían estar conectadas y desplegar una pequeña modificación puede implicar poner en riesgo todo el sistema.
En ese contexto aparece la arquitectura basada en microservicios, una forma de diseñar sistemas como un conjunto de servicios relativamente pequeños, independientes y especializados que colaboran para proporcionar una funcionalidad completa. La idea parece sencilla, pero sus consecuencias son mucho más profundas que separar una aplicación en varios proyectos. Dividir el software también significa dividir responsabilidades, datos, despliegues, fallos y, sobre todo, las decisiones que cada parte del sistema puede tomar de manera independiente.
La pregunta importante, por tanto, no es simplemente cómo crear varios servicios. Es cómo decidir dónde termina uno y empieza otro, cómo conseguir que colaboren sin volver a quedar acoplados y cómo operar todo ese conjunto como un único sistema.
Del sistema único al conjunto de servicios
Imaginemos una aplicación para gestionar una empresa de transporte. En ella podemos encontrar clientes, vehículos, conductores, rutas, pedidos, facturación y seguimiento de entregas. En una aplicación tradicional, toda esa funcionalidad podría vivir dentro de un mismo sistema y compartir una única base de datos.
Mientras el sistema es manejable, esta organización tiene una ventaja evidente: cambiar una parte del programa es relativamente sencillo porque todas las piezas están cerca unas de otras. Una funcionalidad puede llamar directamente a otra clase, consultar una tabla o reutilizar una misma biblioteca. La aplicación se comporta como una unidad.
Pero el crecimiento introduce un problema diferente. Las dependencias empiezan a convertirse en una red difícil de controlar. Una modificación en la gestión de clientes puede afectar a facturación; un cambio en pedidos puede obligar a modificar el seguimiento; una actualización de la base de datos puede tener consecuencias sobre prácticamente toda la aplicación.
El problema no es necesariamente que el código esté mal escrito. Puede existir una arquitectura interna razonablemente organizada y, aun así, el sistema haber alcanzado un tamaño en el que resulta difícil evolucionar determinadas partes de forma independiente.
Una alternativa consiste en dividir el sistema en varios servicios. El servicio de clientes podría encargarse de los clientes; otro, de los pedidos; otro, del seguimiento. Cada uno tendría una responsabilidad claramente delimitada y podría evolucionar y desplegarse de forma independiente.
Aquí aparece una idea fundamental: un microservicio no es simplemente una aplicación pequeña, sino una unidad de software que intenta encapsular una capacidad del negocio y mantener bajo control sus propias dependencias.
Esta distinción es importante porque existe una tentación habitual: coger una aplicación grande, dividir sus clases entre varios proyectos y llamar a cada proyecto “microservicio”. Eso puede producir muchos procesos, pero no necesariamente una arquitectura basada en microservicios. La división física es relativamente sencilla; la verdadera dificultad está en encontrar fronteras que tengan sentido para el negocio y que permitan mantener cierta autonomía.
Las fronteras de servicio: decidir dónde dividir
La primera gran decisión de una arquitectura de microservicios consiste precisamente en decidir qué constituye un servicio.
Podríamos dividir una aplicación atendiendo a criterios puramente técnicos: un servicio para acceder a la base de datos, otro para autenticación, otro para generar informes y otro para enviar correos electrónicos. Algunas de esas separaciones pueden ser perfectamente razonables, pero no existe una regla que diga que cada componente técnico debe convertirse en un microservicio.
La pregunta más interesante es otra: ¿qué parte del problema empresarial queremos que pueda evolucionar con un cierto grado de independencia?
Volvamos a nuestra empresa de transporte. La gestión de pedidos tiene unas reglas, unos datos y unas decisiones propias. El seguimiento de entregas tiene otras. Facturación tiene otras diferentes. Esas áreas pueden interactuar, pero no necesariamente necesitan compartir toda su lógica interna.
Esta idea conduce al concepto de frontera de servicio: el límite dentro del cual un servicio es responsable de una determinada capacidad y fuera del cual debe relacionarse con otros servicios mediante mecanismos explícitos de comunicación.
La frontera es importante porque determina qué conoce cada servicio y qué puede cambiar sin afectar directamente a los demás. Cuanto más clara sea la frontera, mayor será la posibilidad de modificar una parte del sistema sin tener que modificar simultáneamente todas las demás.
Pero las fronteras no son eternas ni evidentes. En un sistema real, podemos descubrir que dos servicios están demasiado relacionados y deberían haberse considerado una única unidad, o que uno de ellos contiene responsabilidades que pertenecen a dominios diferentes. La arquitectura evoluciona a medida que comprendemos mejor el problema.
Por eso, diseñar microservicios no debería empezar con la pregunta “¿cuántos servicios queremos tener?”. Debería empezar por comprender qué capacidades existen, qué reglas pertenecen a cada una y qué relaciones deben existir entre ellas.
Las APIs: la frontera se convierte en un contrato
Una vez que hemos separado dos partes del sistema, aparece inmediatamente otro problema. Antes podían comunicarse mediante llamadas directas a métodos o mediante objetos compartidos. Ahora están separadas.
¿Cómo solicita un servicio de pedidos información a un servicio de clientes?
Aquí entran en juego las API (Application Programming Interfaces). Una API define cómo un software puede interactuar con otro. En una arquitectura de microservicios, la API se convierte en una de las principales formas de expresar el contrato entre dos servicios.
Por ejemplo, el servicio de pedidos podría ofrecer una operación que permita consultar un cliente:
GET /customers/123
El servicio de pedidos no necesita conocer cómo almacena internamente el cliente sus datos. Puede recibir una respuesta como:
{
"id": 123,
"name": "Ana García",
"status": "active"
}
Esta separación parece trivial, pero representa un cambio importante respecto a una arquitectura donde todos los componentes comparten directamente las mismas estructuras internas. El consumidor conoce el contrato que necesita utilizar, pero no debería necesitar conocer la implementación que existe detrás de ese contrato.
Las APIs pueden adoptar diferentes tecnologías y estilos. Una API HTTP basada en REST es una posibilidad habitual, pero también existen otras alternativas, como gRPC o mecanismos basados en mensajería. Lo importante arquitectónicamente no es tanto la tecnología concreta como la existencia de una frontera explícita.
Sin embargo, una API también introduce un coste. Cuando dos componentes están separados, una llamada que antes era local puede convertirse en una comunicación de red. Aparecen latencia, errores de conexión, timeouts y problemas de disponibilidad que no existían dentro del mismo proceso.
Por eso convertir una llamada interna en una llamada de red no es una mejora arquitectónica automática. La comunicación entre servicios debe justificarse por la independencia que proporciona la separación.
Componer servicios para construir una funcionalidad completa
Un sistema de microservicios no resulta útil porque tenga muchos servicios aislados. Su propósito es que esos servicios puedan colaborar.
Supongamos que un usuario realiza un pedido. Para completar la operación quizá necesitemos consultar al servicio de clientes, comprobar el inventario, registrar el pedido y solicitar el pago.
Ningún servicio tiene por qué realizar toda la operación. El sistema puede construir una funcionalidad más compleja mediante la composición de servicios independientes.
Una posibilidad consiste en que un servicio coordine varias llamadas:
Aplicación
|
v
Servicio de Pedidos
|
+--> Servicio de Clientes
|
+--> Servicio de Inventario
|
+--> Servicio de Pagos
Pero esta composición introduce una cuestión fundamental: ¿quién es responsable de coordinar el proceso?
En operaciones sencillas, un servicio puede realizar varias llamadas secuenciales. En procesos más complejos, la coordinación puede requerir mecanismos específicos, especialmente cuando una operación afecta a varios servicios y no podemos utilizar una única transacción de base de datos para garantizar que todo se complete simultáneamente.
Aquí aparecen conceptos como eventos, mensajería y procesamiento asíncrono. En lugar de que un servicio tenga que esperar inmediatamente la respuesta de otro, puede publicar un evento indicando que algo ha ocurrido:
Pedido creado
|
+--> Facturación
|
+--> Notificaciones
|
+--> Seguimiento
Cada consumidor reacciona de forma independiente.
Esta forma de colaboración permite reducir determinadas dependencias temporales, pero tampoco es gratuita. La comunicación asíncrona puede aumentar la autonomía de los servicios a cambio de introducir una mayor complejidad en la coordinación y en la comprensión del estado global del sistema.
Por ejemplo, si el pago se procesa unos segundos después de crear el pedido, el sistema debe asumir que existe un periodo durante el cual el pedido está creado pero todavía no pagado. Esa situación debe formar parte del diseño, no ser tratada como una anomalía inesperada.
La independencia también afecta a los datos
Existe otra frontera especialmente importante: la de los datos.
Si varios microservicios comparten indiscriminadamente las mismas tablas, podemos tener servicios separados físicamente pero unidos por una dependencia muy fuerte. El servicio de pedidos podría conocer las tablas internas del servicio de clientes; facturación podría modificar directamente las tablas de pedidos; cualquier cambio en el esquema podría afectar a todos.
La independencia de un servicio resulta difícil de conseguir si otros servicios dependen directamente de sus datos internos.
Por eso, una arquitectura de microservicios suele intentar que cada servicio tenga autoridad sobre sus propios datos. No significa necesariamente que cada servicio deba utilizar una tecnología de base de datos diferente. Puede haber varias bases de datos del mismo tipo. Lo importante es quién es responsable de cada información y mediante qué contrato pueden acceder a ella los demás.
Esta decisión tiene consecuencias importantes. En una aplicación monolítica podemos consultar varias tablas dentro de una misma transacción. En microservicios, una operación que afecta a varios servicios puede requerir coordinación distribuida.
El resultado es una de las paradojas de esta arquitectura: ganamos independencia local precisamente porque aceptamos una mayor complejidad en las relaciones entre las partes.
Desplegar servicios de forma independiente
La independencia arquitectónica tiene sentido especialmente cuando también podemos desplegar los servicios de manera independiente.
Supongamos que el equipo encargado de notificaciones realiza una modificación. Si la arquitectura está realmente desacoplada, debería poder construir y desplegar una nueva versión del servicio de notificaciones sin tener que reconstruir toda la aplicación.
Este es uno de los principales atractivos de los microservicios. La unidad de despliegue puede coincidir con una unidad de responsabilidad del sistema, permitiendo que diferentes partes evolucionen a ritmos distintos.
Pero aquí aparece otro cambio importante. Una aplicación monolítica puede consistir en un único proceso que desplegamos en unas pocas máquinas. Un sistema formado por veinte servicios necesita gestionar veinte aplicaciones, sus configuraciones, sus versiones, sus redes, sus logs y sus mecanismos de monitorización.
La complejidad que antes estaba dentro de una aplicación empieza a aparecer alrededor de ella.
Por ejemplo, si tenemos diez servicios y cada uno dispone de tres instancias para garantizar disponibilidad, ya no estamos operando diez procesos, sino alrededor de treinta instancias que deben iniciarse, detenerse, actualizarse y supervisarse correctamente.
La arquitectura necesita entonces mecanismos que permitan automatizar esta operación.
La orquestación: cuando administrar servicios se convierte en un problema propio
Aquí entra en escena la orquestación. Orquestar significa coordinar de forma automatizada la ejecución de múltiples servicios o contenedores: dónde se ejecutan, cuántas instancias existen, qué ocurre si una falla, cómo reciben configuración o cómo se realiza una actualización.
Tecnologías como Kubernetes han adquirido una gran importancia precisamente porque permiten automatizar buena parte de estas tareas.
Podemos imaginar un servicio que deja de responder. En un sistema sencillo, alguien podría tener que reiniciarlo manualmente. En un entorno orquestado, la plataforma puede detectar que una instancia ha desaparecido y crear otra para mantener el estado deseado.
La idea es especialmente poderosa cuando el número de servicios crece. Los microservicios no eliminan la complejidad operativa: la trasladan a una capa que debe automatizarse y gestionarse explícitamente.
Esto explica también por qué contenedores y orquestadores aparecen frecuentemente asociados a los microservicios. Un contenedor facilita empaquetar y ejecutar una aplicación de manera reproducible, mientras que un orquestador permite administrar grandes cantidades de esas unidades.
Pero conviene evitar una confusión habitual: Kubernetes no convierte una aplicación en microservicios. Podemos ejecutar una aplicación monolítica en Kubernetes y podemos construir microservicios sin Kubernetes. La arquitectura y la plataforma de ejecución son decisiones relacionadas, pero no son la misma decisión.
Microservicios no significa dividir por dividir
Llegados a este punto aparece una pregunta inevitable: si dividir permite obtener independencia, ¿por qué no dividir cualquier sistema en decenas o cientos de servicios?
Porque cada frontera tiene un precio.
Cada nuevo servicio necesita despliegue, configuración, monitorización, seguridad, gestión de errores y posiblemente almacenamiento propio. Además, cada comunicación entre servicios introduce latencia y nuevos puntos de fallo.
Imaginemos que una operación aparentemente sencilla necesita atravesar seis servicios:
A → B → C → D → E → F
Si cualquiera de ellos falla, la operación completa puede verse afectada. El sistema distribuido puede ser mucho más complejo que la aplicación que pretendíamos mejorar.
Este fenómeno conduce a una idea esencial: el objetivo no es maximizar el número de microservicios, sino encontrar fronteras que proporcionen suficiente autonomía como para justificar la complejidad adicional que introducen.
Un servicio demasiado grande puede parecer un monolito dentro del sistema distribuido. Pero uno demasiado pequeño puede acabar convirtiendo una operación sencilla en una cadena de llamadas de red difíciles de comprender y mantener.
Por eso el tamaño físico del servicio es un criterio secundario. Mucho más importante es su cohesión: qué responsabilidades contiene, qué reglas gobierna y qué razones existen para que esas responsabilidades cambien juntas.
Una arquitectura distribuida cambia la naturaleza de los errores
En una aplicación que vive dentro de un único proceso, muchas cosas pueden darse por garantizadas. Una llamada a una función puede fallar, pero la comunicación en sí misma no suele depender de una red.
En microservicios, la red pasa a formar parte del comportamiento cotidiano del sistema.
Un servicio puede estar temporalmente indisponible. Una petición puede tardar demasiado. Un mensaje puede procesarse más tarde. Una instancia puede reiniciarse mientras una operación está en curso.
Esto obliga a introducir mecanismos como timeouts, reintentos, circuit breakers, idempotencia o trazabilidad distribuida.
La idempotencia, por ejemplo, permite diseñar una operación de manera que repetirla no produzca efectos adicionales no deseados. Resulta especialmente importante cuando un cliente no sabe si una petición ha llegado correctamente y decide volver a enviarla.
La observabilidad también adquiere una importancia mucho mayor. En una aplicación monolítica podemos revisar un log y seguir una operación dentro del mismo proceso. En un sistema distribuido, una única petición del usuario puede atravesar varios servicios y generar decenas de acontecimientos que necesitamos relacionar para entender qué ha ocurrido.
De ahí la importancia de métricas, logs centralizados y trazas distribuidas.
La historia explica por qué esta arquitectura resulta atractiva
La arquitectura de microservicios no apareció de forma aislada. Es, en buena medida, una respuesta a la evolución de los sistemas y de las organizaciones de desarrollo.
Durante años, las aplicaciones empresariales tendieron a crecer como grandes unidades. Posteriormente, prácticas como Service-Oriented Architecture (SOA) popularizaron la idea de construir sistemas a partir de servicios. La experiencia adquirida con estos sistemas, junto con la expansión de la computación en la nube, la automatización del despliegue y los contenedores, creó las condiciones para una aproximación más ligera y orientada a servicios pequeños.
El término “microservices” comenzó a popularizarse durante la década de 2010, especialmente en torno a experiencias de organizaciones que necesitaban desplegar grandes sistemas con equipos capaces de trabajar de forma relativamente autónoma.
La evolución resulta interesante porque los microservicios no surgieron simplemente porque dividir software fuese una idea nueva, sino porque determinadas organizaciones necesitaban que equipos y partes del sistema pudieran evolucionar a una velocidad y con un nivel de independencia que una arquitectura monolítica dificultaba.
Esto también explica por qué los microservicios están estrechamente relacionados con prácticas como integración continua, entrega continua, automatización y equipos responsables de sus servicios. La arquitectura tiene una dimensión técnica, pero también organizativa.
Microservicios y arquitectura monolítica no son enemigos
A veces se presenta la arquitectura monolítica como una etapa que necesariamente debe superarse. Esa interpretación puede llevar a decisiones poco justificadas.
Un monolito bien diseñado puede ser una solución excelente para determinados sistemas. Puede ser más sencillo de desarrollar, probar, desplegar y operar. Una única base de datos puede simplificar las transacciones y determinadas consultas.
Los microservicios empiezan a resultar especialmente interesantes cuando necesitamos independencia de evolución, despliegues separados, escalado diferenciado o autonomía entre determinadas partes del sistema.
Pero si esas necesidades no existen, introducir una arquitectura distribuida puede aportar más complejidad que beneficios.
De hecho, existe una estrategia intermedia especialmente útil: comenzar con un monolito bien modularizado y establecer fronteras claras dentro de él. Si posteriormente alguna de esas fronteras necesita convertirse en un servicio independiente, la separación conceptual ya existe y la extracción resulta más sencilla.
La decisión arquitectónica deja así de ser “monolito o microservicios” y pasa a ser una cuestión más concreta: ¿qué grado de independencia necesita cada parte del sistema y cuánto estamos dispuestos a pagar en complejidad para conseguirlo?
Qué cambia realmente cuando adoptamos microservicios
Visto desde fuera, una arquitectura basada en microservicios parece consistir en cambiar una aplicación por muchas aplicaciones pequeñas. Pero esa descripción se queda corta.
Cambia la manera de definir responsabilidades. Cambia la forma en que los componentes se comunican. Cambia la relación con los datos. Cambia el modelo de despliegue. Cambia la gestión de errores. Cambia incluso la forma de trabajar de los equipos.
En un sistema monolítico, muchas decisiones pueden permanecer implícitas porque todos los componentes están dentro del mismo espacio. En un sistema distribuido, las fronteras obligan a hacer explícitos contratos, dependencias y responsabilidades que antes podían permanecer ocultos dentro del código.
Ese es probablemente uno de los aspectos más importantes de esta arquitectura. Los microservicios no son principalmente una técnica para conseguir programas más pequeños. Son una forma de construir sistemas donde determinadas partes puedan tener un ciclo de vida más independiente.
Una forma diferente de entender la arquitectura
Una arquitectura basada en microservicios puede resumirse visualmente como varios servicios colaborando:
┌─────────────────┐
│ Cliente │
└────────┬────────┘
│
▼
┌─────────────────┐
│ API / Gateway │
└────────┬────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Clientes │ │ Pedidos │ │ Pagos │
│ │ │ │ │ │
│ Datos propios │ │ Datos propios │ │ Datos propios │
└────────────────┘ └────────────────┘ └────────────────┘
│ │ │
└──────────────────┼──────────────────┘
▼
Mensajería / Eventos
│
▼
┌─────────────────┐
│ Notificaciones │
└─────────────────┘
Pero el diagrama no debería hacernos perder de vista la idea fundamental. La arquitectura basada en microservicios no consiste en dibujar muchos cuadrados, sino en establecer fronteras significativas entre capacidades que necesitan evolucionar con cierto grado de independencia.
Las APIs hacen explícita la comunicación entre esas fronteras. La composición permite que varios servicios colaboren para proporcionar funcionalidades mayores. La separación de datos evita que la independencia sea solamente superficial. El despliegue independiente permite que cada parte evolucione a su propio ritmo. Y la orquestación proporciona la infraestructura necesaria para operar todo ese conjunto sin convertir la administración en una tarea manual e inmanejable.
El precio es claro: distribuimos también los problemas. Aparecen latencia, fallos de red, consistencia eventual, observabilidad distribuida y una infraestructura considerablemente más compleja.
Por eso entender microservicios no consiste en aprender a crear servicios pequeños. Consiste en comprender cuándo una frontera aporta suficiente independencia como para compensar el coste de introducirla. Cuando esa decisión está bien fundamentada, los microservicios pueden convertirse en una herramienta para controlar la evolución de sistemas grandes. Cuando la división responde únicamente a una moda tecnológica o al deseo de tener “muchos servicios”, el resultado puede ser simplemente un sistema distribuido que conserva la complejidad del monolito y añade todos los problemas de la red.
La verdadera arquitectura, como tantas veces ocurre en ingeniería de software, no está en la tecnología que elegimos, sino en las decisiones sobre responsabilidades, dependencias y límites que hacemos visibles mediante esa tecnología.