← Todos los temas
Día 4 · Cómo habla el software

Monolito o microservicios

Arquitectura monolítica y de microservicios: monolito modular, acoplamiento y cohesión, límites por dominio, comunicación síncrona y asíncrona, base de datos por servicio, consistencia eventual, escalado independiente, costos operativos y cuándo migrar.

Definición

La arquitectura de software es la organización de una aplicación en componentes y de las reglas con que se comunican. Una de las decisiones estructurales más influyentes es cómo se empaqueta y se despliega el código.

Una arquitectura monolítica (monolith) construye y despliega toda la aplicación como una sola unidad: interfaz, reglas de negocio y acceso a datos viven en un mismo programa, que se ejecuta como un único proceso y suele usar una sola base de datos.

Una arquitectura de microservicios (microservices) divide la aplicación en servicios pequeños, cada uno con una responsabilidad acotada, que se ejecutan como procesos independientes y se comunican por la red mediante interfaces bien definidas, normalmente APIs HTTP o mensajes. Martin Fowler y James Lewis la describen como un conjunto de servicios que se pueden desplegar de forma independiente y que se organizan en torno a capacidades de negocio.

Ninguna de las dos es «mejor» en abstracto: cada una desplaza la complejidad a un lugar distinto. El monolito la concentra dentro del código; los microservicios la trasladan a la red y a la operación.

El monolito modular

Un monolito no es sinónimo de código desordenado. Un monolito modular (modular monolith) se despliega como una sola unidad, pero su código interno se divide en módulos con fronteras explícitas: cada módulo expone una interfaz pública y oculta su implementación y sus tablas. Las llamadas entre módulos son llamadas a función en el mismo proceso: rápidas, transaccionales y fáciles de depurar.

Es el punto de partida recomendado para la mayoría de los sistemas, porque conserva las ventajas del monolito (una sola implantación, una sola transacción, depuración sencilla) y deja preparadas las costuras por donde, si hace falta, se podrá separar más adelante.

Acoplamiento y cohesión

Dos conceptos permiten juzgar cualquier división, sea en módulos o en servicios:

  • Acoplamiento (coupling): grado en que un componente depende de los detalles internos de otro. Con bajo acoplamiento, cambiar un componente no obliga a modificar los demás.
  • Cohesión (cohesion): grado en que los elementos de un componente pertenecen a una misma responsabilidad. Con alta cohesión, todo lo que cambia por la misma razón está junto.

El objetivo es alta cohesión dentro de cada pieza y bajo acoplamiento entre piezas. Partir un sistema en servicios sin lograrlo produce un monolito distribuido (distributed monolith): servicios que deben desplegarse y cambiarse en bloque, pero que además se comunican por una red falible. Es el peor de los dos mundos.

Límites por dominio

¿Por dónde se corta? El criterio útil no es técnico (un servicio de «base de datos», otro de «interfaz»), sino de dominio: el área del negocio que el software modela. El diseño guiado por el dominio (Domain-Driven Design, DDD) propone dividir el sistema en contextos delimitados (bounded contexts): fronteras dentro de las cuales cada término tiene un significado único y un modelo propio.

Por ejemplo, «producto» significa algo distinto para el catálogo (descripción, fotos), para el inventario (existencias, ubicación) y para facturación (precio, impuestos). Cada contexto mantiene su propio modelo y se relaciona con los demás por contratos explícitos. Un contexto delimitado es un buen candidato a módulo y, llegado el caso, a servicio.

Comunicación entre servicios

Una llamada dentro de un proceso casi nunca falla; una llamada por la red puede tardar, perderse o no recibir respuesta. Hay dos estilos de comunicación:

  • Síncrona: quien llama envía una petición y espera la respuesta. Se implementa con REST (HTTP con JSON, el estilo del día 2) o con gRPC (llamadas a procedimientos remotos sobre HTTP/2 con mensajes binarios, más compactas y con contrato tipado). Es simple de razonar, pero acopla en el tiempo: si el servicio llamado está caído, quien llama se queda sin respuesta.
  • Asíncrona: quien envía publica un mensaje o evento en una cola o un bus de eventos y continúa sin esperar; otros servicios lo consumen cuando pueden. Reduce el acoplamiento temporal y absorbe picos de carga, a cambio de mayor dificultad para seguir el flujo completo. El día 6 trata las colas y los eventos.

Regla práctica: síncrono para consultas que necesitan respuesta inmediata; asíncrono para notificar hechos («pedido creado») a quienes deban reaccionar.

Datos: una base por servicio

En microservicios, cada servicio es dueño exclusivo de sus datos (database per service): ningún otro servicio lee o escribe sus tablas directamente; solo lo hace a través de su API. Si dos servicios comparten tablas, quedan acoplados por el esquema y se pierde la independencia de despliegue.

Esto tiene una consecuencia profunda: ya no existe una única transacción ACID (día 3) que abarque, por ejemplo, el pedido y el inventario. Se adopta consistencia eventual (eventual consistency): los datos de los distintos servicios pueden estar momentáneamente desalineados y convergen tras un breve intervalo.

Para operaciones de negocio que cruzan servicios se usa el patrón saga (saga pattern): una secuencia de transacciones locales, una por servicio, donde cada paso publica el resultado y, si un paso falla, se ejecutan transacciones compensatorias que deshacen los pasos previos (por ejemplo, liberar la reserva de inventario si el pago es rechazado). Una saga puede coordinarse por eventos (coreografía) o mediante un orquestador central (orquestación).

Despliegue y escalado independientes

Es la principal ventaja de los microservicios:

  • Despliegue independiente: un equipo publica su servicio sin coordinarse con los demás ni redesplegar todo el sistema; una falla de publicación afecta a una pieza, no a la aplicación entera.
  • Escalado independiente: se replica solo el servicio que está saturado. Si el cuello de botella es la búsqueda de catálogo, se multiplican sus instancias sin duplicar el módulo de facturación.
  • Tecnología a la medida: cada servicio puede usar el lenguaje o la base de datos más adecuados, con el costo de mantener más variedad.

Con un monolito, escalar significa replicar la aplicación completa, aunque solo una parte lo necesite. Los contenedores (día 5) y los orquestadores son la herramienta habitual para operar muchas instancias.

API gateway

Cuando existen decenas de servicios, los clientes (aplicación móvil, sitio web) no deberían conocer la dirección de cada uno. Un API gateway es un punto de entrada único que recibe las peticiones externas y las enruta al servicio correspondiente. Además centraliza funciones transversales: autenticación, limitación de tasa (rate limiting), cifrado TLS, registro de peticiones y, si se necesita, agregación de respuestas de varios servicios en una sola.

Observabilidad

En un monolito, una traza de error muestra la ruta completa de ejecución. En microservicios, una sola petición del usuario atraviesa varios procesos y servidores, y un fallo puede originarse tres servicios atrás. La observabilidad (observability) es la capacidad de inferir el estado interno del sistema a partir de lo que emite, y se apoya en tres señales: registros (logs), métricas y trazas distribuidas (distributed tracing).

Una traza distribuida asigna a cada petición un identificador de correlación que viaja en las cabeceras de todas las llamadas internas, de modo que se puede reconstruir el recorrido completo y ver cuánto tardó cada servicio. Sin esto, depurar microservicios es muy costoso. El día 14 lo desarrolla.

Costos de los microservicios

Antes de adoptarlos conviene conocer lo que cuestan:

  • Latencia de red: una llamada remota tarda de milisegundos a decenas de milisegundos, frente a nanosegundos de una llamada local. Una cadena de servicios acumula esas esperas.
  • Fallas parciales (partial failures): en un monolito el sistema funciona o no funciona; en una arquitectura distribuida un servicio puede caer mientras los demás siguen activos. Hay que diseñar para ello con tiempos de espera (timeouts), reintentos, cortacircuitos (circuit breakers) y respuestas degradadas.
  • Complejidad operativa: decenas de servicios implican decenas de despliegues, configuraciones, versiones de contrato, paneles de monitoreo y alertas. Exige automatización madura (CI/CD, día 7).
  • Consistencia y pruebas: las pruebas de extremo a extremo se vuelven más lentas y frágiles; los datos dejan de ser transaccionales entre servicios.

La ley de Conway (Melvin Conway, 1967) establece que las organizaciones diseñan sistemas cuya estructura copia la de su comunicación interna. Tiene una lectura práctica: la arquitectura de servicios se sostiene cuando coincide con la de los equipos. Un servicio con dueño claro funciona; un servicio compartido por cinco equipos que deben ponerse de acuerdo para cada cambio, no. Los microservicios resuelven sobre todo un problema de escala organizacional, no solo técnico.

Cuándo migrar

Martin Fowler observó que casi todos los microservicios exitosos empezaron como un monolito que creció demasiado, y que casi todos los sistemas construidos como microservicios desde cero tuvieron problemas serios. De ahí su principio MonolithFirst: empieza con un monolito (modular) y separa servicios cuando el dominio ya esté comprendido y haya una razón concreta, porque los límites correctos solo se conocen con el uso.

Señales de que separar tiene sentido:

  • Varios equipos se bloquean mutuamente al cambiar y publicar el mismo código.
  • Una parte tiene una carga o requisitos de escalado muy distintos al resto.
  • Una parte necesita un ciclo de publicación mucho más rápido o una tecnología distinta.
  • Un módulo ya tiene límites de dominio nítidos y casi ningún acoplamiento con los demás.

Para migrar sin reescribir todo se usa el patrón strangler fig («higuera estranguladora»): delante del monolito se coloca una capa de enrutamiento y se extrae una funcionalidad a la vez a un servicio nuevo; el enrutador desvía a él ese tráfico mientras el resto sigue en el monolito. Con el tiempo el monolito se reduce hasta poder retirarse, y en cada paso el sistema sigue funcionando.

Comparativa

CriterioMonolito (modular)Microservicios
DespliegueUna sola unidadUn despliegue por servicio
EscaladoSe replica toda la aplicaciónSe replica solo el servicio saturado
Comunicación internaLlamadas a función en un procesoLlamadas por red (HTTP, gRPC) o mensajes
Datos y transaccionesUna base de datos, transacciones ACIDUna base por servicio, consistencia eventual, sagas
DepuraciónUna traza de ejecuciónRequiere trazas distribuidas
FallasTodo o nadaFallas parciales que hay que tolerar
Complejidad operativaBajaAlta
Autonomía de equiposLimitada por el código compartidoAlta, si los límites son correctos
Adecuado paraEquipos pequeños y medianos, dominio en exploraciónOrganizaciones grandes, dominios estables, escalas desiguales

Práctica: separar una API en dos servicios

Se parte de una aplicación de pedidos y se separa el inventario en su propio servicio. pedidos ya no consulta una tabla: llama por HTTP a inventario. Todo el código de esta guía se ejecutó con Python 3.12, FastAPI, httpx y pytest.

Servicio inventario (archivo inventario.py):

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title="inventario")

STOCK = {"TACO-01": 10, "SALSA-02": 0}


class Reserva(BaseModel):
    sku: str
    cantidad: int = Field(gt=0)


@app.get("/stock/{sku}")
def consultar_stock(sku: str):
    if sku not in STOCK:
        raise HTTPException(status_code=404, detail="SKU desconocido")
    return {"sku": sku, "disponible": STOCK[sku]}


@app.post("/reservas")
def reservar(reserva: Reserva):
    disponible = STOCK.get(reserva.sku)
    if disponible is None:
        raise HTTPException(status_code=404, detail="SKU desconocido")
    if disponible < reserva.cantidad:
        raise HTTPException(status_code=409, detail="Stock insuficiente")
    STOCK[reserva.sku] = disponible - reserva.cantidad
    return {"sku": reserva.sku, "reservado": reserva.cantidad}

Servicio pedidos (archivo pedidos.py), que depende de un cliente HTTP inyectado con Depends:

import os

import httpx
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title="pedidos")

INVENTARIO_URL = os.getenv("INVENTARIO_URL", "http://127.0.0.1:8001")
PEDIDOS: list[dict] = []


class NuevoPedido(BaseModel):
    sku: str
    cantidad: int = Field(gt=0)


def get_inventario():
    with httpx.Client(base_url=INVENTARIO_URL, timeout=2.0) as cliente:
        yield cliente


@app.post("/pedidos", status_code=201)
def crear_pedido(pedido: NuevoPedido, inventario: httpx.Client = Depends(get_inventario)):
    try:
        r = inventario.post("/reservas", json=pedido.model_dump())
    except httpx.TransportError:
        raise HTTPException(status_code=503, detail="Inventario no disponible")
    if r.status_code in (404, 409):
        raise HTTPException(status_code=r.status_code, detail=r.json()["detail"])
    r.raise_for_status()
    registro = {"id": len(PEDIDOS) + 1, **pedido.model_dump()}
    PEDIDOS.append(registro)
    return registro

Tres decisiones de diseño: el cliente tiene un tiempo de espera de 2 segundos; los errores de negocio de inventario (404, 409) se traducen y propagan; y cualquier falla de transporte (conexión rechazada, tiempo agotado) se convierte en 503 Service Unavailable, la respuesta honesta ante una falla parcial.

Pruebas (archivo test_servicios.py). Como TestClient es un cliente httpx, se inyecta en pedidos el TestClient de inventario: las dos aplicaciones se hablan sin abrir puertos. Para simular la caída se usa httpx.MockTransport:

import httpx
import pytest
from fastapi.testclient import TestClient

import inventario
import pedidos


@pytest.fixture(autouse=True)
def estado_limpio():
    inventario.STOCK.clear()
    inventario.STOCK.update({"TACO-01": 10, "SALSA-02": 0})
    pedidos.PEDIDOS.clear()
    yield
    pedidos.app.dependency_overrides.clear()


def pedidos_con(cliente_inventario):
    pedidos.app.dependency_overrides[pedidos.get_inventario] = lambda: cliente_inventario
    return TestClient(pedidos.app)


def test_inventario_por_separado():
    c = TestClient(inventario.app)
    assert c.get("/stock/TACO-01").json() == {"sku": "TACO-01", "disponible": 10}
    assert c.get("/stock/NOEXISTE").status_code == 404


def test_pedido_reserva_en_inventario():
    c = pedidos_con(TestClient(inventario.app))
    r = c.post("/pedidos", json={"sku": "TACO-01", "cantidad": 3})
    assert r.status_code == 201
    assert r.json() == {"id": 1, "sku": "TACO-01", "cantidad": 3}
    assert inventario.STOCK["TACO-01"] == 7


def test_stock_insuficiente_propaga_409():
    c = pedidos_con(TestClient(inventario.app))
    r = c.post("/pedidos", json={"sku": "SALSA-02", "cantidad": 1})
    assert r.status_code == 409
    assert pedidos.PEDIDOS == []


def test_inventario_caido_responde_503():
    def caido(request):
        raise httpx.ConnectError("conexión rechazada", request=request)

    falso = httpx.Client(base_url="http://inventario", transport=httpx.MockTransport(caido))
    c = pedidos_con(falso)
    r = c.post("/pedidos", json={"sku": "TACO-01", "cantidad": 1})
    assert r.status_code == 503
    assert r.json() == {"detail": "Inventario no disponible"}
    assert pedidos.PEDIDOS == []

Ejecución:

  1. Instala las dependencias: python -m pip install fastapi uvicorn httpx pytest
  2. Con los tres archivos en la misma carpeta: python -m pytest -v. Resultado esperado: 4 pruebas aprobadas (4 passed).
  3. Para levantarlos de verdad, abre dos terminales en esa carpeta. En la primera: python -m uvicorn inventario:app --port 8001. En la segunda: python -m uvicorn pedidos:app --port 8000.
  4. Envía un pedido real: curl -X POST http://127.0.0.1:8000/pedidos -H "Content-Type: application/json" -d "{\"sku\":\"TACO-01\",\"cantidad\":3}". Responde 201 con {"id":1,"sku":"TACO-01","cantidad":3}; en el registro de inventario aparece la llamada POST /reservas.
  5. Detén inventario con Ctrl+C y repite la petición: pedidos sigue vivo y responde 503 con {"detail":"Inventario no disponible"}. Es una falla parcial: una pieza cae y el resto del sistema lo comunica con claridad.

Ejercicio: agrega un servicio pagos y reflexiona qué debería ocurrir con la reserva de inventario si el pago es rechazado. Esa pregunta es, en pequeño, el patrón saga.

Preguntas frecuentes

¿Qué es un monolito en software?

Es una aplicación que se construye y se despliega como una sola pieza: todas sus funciones comparten el mismo código y, normalmente, la misma base de datos.

¿Qué son los microservicios?

Es una arquitectura en la que el sistema se divide en servicios pequeños e independientes, cada uno con su propia responsabilidad y sus datos, que se comunican por la red y se despliegan por separado.

¿Cómo aguanta Netflix a millones de personas a la vez?

Una de las razones es que reparte su sistema en muchos servicios independientes: cada uno se escala por su cuenta según la demanda y, si uno falla, el resto puede seguir funcionando.

¿Siempre es mejor usar microservicios?

No. Suman costos de red, monitoreo, despliegue y coordinación entre equipos. Para equipos pequeños o productos que empiezan, un monolito bien organizado (modular) suele ser la mejor opción.

En el reel lo explicamos así

Un puesto de tacos: una sola persona toma la orden, cocina y cobra. Mientras no haya mucha gente funciona de maravilla, y es barato y simple. Pero en la hora pico se forma la fila, y si esa persona se enferma, se detiene todo.

Una cocina grande trabaja por estaciones: carne, salsas y caja. Cada una tiene su responsable y se comunican con la comanda. Si la caja falla, siguen preparándose tacos. Y en la hora pico no se duplica toda la cocina: solo crece la estación de carne. A cambio, hay más gente que coordinar, y las estaciones deben entenderse bien entre sí.

Referencias

En el reel lo explicamos así

Un puesto de tacos donde una persona hace todo, contra una cocina grande con estaciones: carne, salsas, caja.

Glosario: del inglés al español

Monolithmonolito (un solo bloque)
Microservicesmicroservicios (piezas pequeñas)
Scaleescalar (crecer para atender más)