Architecture

Microservicios

Los microservicios dividen una aplicación en servicios desplegables de forma independiente basados en capacidades de negocio. El objetivo es la autonomía del equipo y los lanzamientos independientes, no el escalado por el simple hecho de escalar; y la red entre ellos es el precio a pagar.

advanced16 min readUpdated 16 sept 2026
services/orders/service.yaml
yaml
# services/orders/service.yaml
name: orders
image: ghcr.io/acme/orders:1.14.2
port: 8080
replicas: 3

env:
  DATABASE_URL: secret://orders-db
  BROKER_URL: kafka://events:9092
  OTEL_EXPORTER_OTLP_ENDPOINT: http://otel:4317

resources:
  requests: { cpu: "250m", memory: "256Mi" }
  limits: { cpu: "1", memory: "512Mi" }

probes:
  liveness: /healthz
  readiness: /readyz

dependencies:
  inventory: http://inventory:8080
  payments: http://payments:8080
Unidad de despliegue
Un servicio, un pipeline
Propiedad de datos
Una base de datos por servicio
Comunicaciones por defecto
HTTP o gRPC, luego eventos
Problema más difícil
Transacciones distribuidas
Modo de fallo
Fallo parcial
Estructura del equipo
Equipos pequeños y autónomos

Por que importa

Lo que realmente ganas con la independencia

Desplegables independientemente

Cada servicio se lanza según su propio calendario. Un cambio en el servicio de envíos no espera a un tren de lanzamientos que incluya facturación y catálogo.

Propiedad de extremo a extremo

Un equipo pequeño es dueño del código, los datos y la guardia (on-call) de un servicio. La propiedad clara es lo que hace que la autonomía sea real y no nominal.

Comunicación vía red

Los servicios se integran a través de contratos explícitos y eventos en lugar de memoria compartida o tablas compartidas, lo cual es a la vez el beneficio y el costo.

La imagen completa

Las tres fuerzas detrás de un límite de servicio

Un límite es una capacidad de negocio, un contrato explícito y un flujo de eventos que desacopla los servicios a ambos lados.

Capacidad de negocio

Límite

Un servicio es dueño de una capacidad y del lenguaje que la describe. El límite sigue al dominio, no a una capa técnica.

Contrato

Interfaz

Los clientes dependen de un contrato versionado, nunca de los internos del servicio. Los cambios aditivos mantienen el funcionamiento de los consumidores.

Eventos

Desacoplar

Un hecho publicado permite que otros servicios reaccionen sin que el productor sepa quién escucha, eliminando el acoplamiento temporal.

HTML5 de un vistazo

Las piezas móviles que ahora operas

Servicios

Unidades desplegables pequeñas, cada una con su propio ciclo de vida y repo.

Gateway

Un único punto de entrada para enrutamiento, auth y límites de tasa (rate limits).

Discovery

Los nombres se resuelven a instancias saludables vía DNS o un registro.

Almacenes de datos

Cada servicio es dueño de su esquema y nadie más escribe en él.

Bus de eventos

Tópicos duraderos transportan hechos entre servicios de forma asíncrona.

Trazado

Los IDs de correlación siguen una solicitud a través de cada salto.

Flujo

Una solicitud a través de los servicios

Una sola acción del usuario se convierte en varias llamadas de red, cada una de las cuales puede fallar por sí misma. El gateway gestiona el tiempo límite (deadline) y la alternativa (fallback).

  1. 1

    El gateway recibe la solicitud

    Autentica al llamador, aplica límites de tasa y enruta la llamada al primer servicio.

  2. 2

    El Servicio A inicia el trabajo

    Valida la entrada y escribe en su propia base de datos. Hasta aquí nada es distribuido excepto el punto de entrada.

  3. 3

    A necesita datos de B

    Llama a B síncronamente con un timeout o publica un evento y continúa. La elección decide qué tan acoplados están los dos.

  4. 4

    B puede llamar a C

    La cadena crece. Cada salto extra añade latencia y otra oportunidad de fallo parcial que A debe manejar.

  5. 5

    Cada servicio puede fallar solo

    B puede estar caído mientras A está saludable. A necesita un timeout, una política de reintentos y un fallback exactamente para este caso.

  6. 6

    El gateway agrega y devuelve

    Combina los resultados bajo un mismo tiempo límite, degradándose elegantemente en lugar de quedar colgado cuando un servicio downstream es lento.

Una breve historia

Cómo llegó la industria hasta aquí

  1. 2011

    El término se consolida

    Un taller de arquitectos nombra el estilo, y la idea de servicios pequeños basados en capacidades de negocio se difunde a través de charlas en conferencias.

    11
  2. 2014

    Definición mainstream

    Martin Fowler y James Lewis publican el artículo canónico, y Docker hace que el empaquetado de un servicio sea trivial.

    14
  3. 2015

    Los contenedores cambian la unidad

    Docker y los primeros orquestadores convierten el despliegue de una operación especial a una rutinaria, que es lo que hace prácticos a los múltiples servicios.

    15
  4. 2017

    La orquestación se consolida

    Kubernetes se convierte en el programador (scheduler) por defecto, y el service discovery, el escalado y el rollout dejan de ser problemas personalizados.

    17
  5. 2019

    Service mesh y trazado

    Los sidecars mueven los reintentos, mTLS y la telemetría fuera del código de la aplicación, y OpenTelemetry estandariza el trazado distribuido.

    19
  6. 2022

    Una corrección

    Los equipos que dividieron demasiado pronto hablan abiertamente sobre el monolito modular, y la conversación pasa de "qué tan pequeño" a "qué tan desplegable independientemente".

    22

La guia completa

Microservicios: Todo lo que necesitas saber

Qué son los microservicios y qué no son

Los microservicios son un estilo arquitectónico en el que una aplicación se compone de servicios desplegables de forma independiente, organizados en torno a capacidades de negocio, donde cada uno posee sus propios datos y se comunica a través de la red. La palabra clave aquí es independientemente. Un servicio que no puede desplegarse por sí solo no es un microservicio; es un módulo con un límite de red.

Vale la pena mencionar qué no es este estilo. No es una regla que los servicios deban ser pequeños. No es un requisito utilizar contenedores, Kubernetes o un service mesh, aunque estas herramientas existen porque este estilo es engorroso sin ellas. No es automáticamente más escalable, más fiable o más moderno que un monolito. Esas son propiedades que se construyen, no propiedades que se obtienen simplemente por la topología.

El único rasgo definitorio es que un cambio en un servicio puede llegar a producción sin necesidad de un lanzamiento coordinado de los demás. Todo lo demás —los límites, los contratos, los eventos, la plataforma— existe para hacer que ese rasgo sea realidad y para sobrevivir a sus consecuencias.

Un ejemplo de descomposición

Tomemos una tienda online. Las capacidades son fáciles de nombrar y cada una se convierte en un servicio con un responsable claro.

catalog        products, prices, availability
cart           the pre-purchase basket
orders         the placed order and its lifecycle
inventory      stock levels and reservations
payments       charges, refunds, payment methods
shipping       labels, carriers, tracking
notifications  email, push, SMS
identity       accounts, sessions, API keys

Fíjate en lo que falta en esa lista. No hay un “servicio de base de datos”, ni un “servicio de envío de emails” —las notificaciones son una capacidad, no un transporte— ni un “servicio de usuarios” que todos los demás servicios llamen solo para renderizar un nombre. Cada entrada es dueña de una parte del negocio, y un equipo puede hacerse cargo de uno o dos de ellos de principio a fin.

Las interacciones importan tanto como la lista. Navegar por el catálogo es una tarea intensiva en lectura y puede cachearse agresivamente. Realizar un pedido es una escritura que coordina el inventario, los pagos y el envío. Las notificaciones son un consumidor puro: reacciona a eventos y nunca es llamado. Estas diferentes formas justifican tratamientos distintos —caché, sagas, colas— a pesar de que todos sean “servicios”.

El verdadero motor es la capacidad de despliegue independiente

A menudo, los equipos adoptan microservicios “para escalar”, solo para descubrir que nunca necesitaron ese escalado. La razón honesta para dividir es organizativa. Cuando diez ingenieros trabajan en un mismo despliegue, cada lanzamiento depende del cambio más lento, y un test fallido en un área bloquea a todo el mundo. Dividir por capacidades otorga a cada equipo su propio pipeline, su propio sistema de guardias (on-call) y su propio ritmo.

Esto es la ley de Conway a la inversa: diseñas el sistema para que sus costuras coincidan con la forma en que se comunican tus equipos. Si un solo equipo es dueño de todo el producto, los microservicios solo te traen fallos de red y una plataforma que mantener a cambio de nada. Si seis equipos necesitan hacer despliegues de forma independiente, las costuras empiezan a rentabilizarse.

Por lo tanto, la primera pregunta no es “¿cómo descomponemos este dominio?”, sino “¿qué partes de este sistema necesitan genuinamente cambiar a ritmos diferentes y ser gestionadas por personas distintas?”. Solo esas respuestas se convierten en servicios.

Los límites siguen las capacidades de negocio

La costura adecuada es una capacidad de negocio, no una capa técnica. La facturación, el envío, el catálogo, la identidad y las notificaciones son capacidades. Un “servicio de base de datos”, un “servicio de email” o un “servicio de validación” es una capa técnica disfrazada de servicio, y cada funcionalidad requerirá cambiar tres de ellos a la vez.

El diseño orientado al dominio (Domain-driven design) nos proporciona el vocabulario. Un bounded context es un límite dentro del cual un modelo particular y su lenguaje son consistentes. “Pedido” en el contexto de ventas significa un carrito a punto de ser pagado; en el contexto de logística significa un paquete para recoger y enviar. Ambos son correctos dentro de su propio contexto, y forzar una única clase Order compartida entre ambos es la forma en que un monolito se convierte en un caos.

Un buen límite tiene tres propiedades:

  • Posee un conjunto coherente de reglas que cambian juntas.
  • Tiene una interfaz pequeña y estable en relación con la cantidad de lógica que hay detrás.
  • Puede ser comprendido por un solo equipo sin necesidad de leer el resto del sistema.

Los límites son costosos de mover una vez que otros servicios dependen de ellos, por lo que es recomendable optar por servicios menos numerosos y más grandes al principio. Dividir un servicio es fácil; fusionar dos que han divergido es doloroso.

Comunicación síncrona: REST y gRPC

La interacción más sencilla es una solicitud y una respuesta. Utiliza REST con JSON para cualquier cosa con la que interactúe un navegador o un cliente externo, y gRPC cuando ambos lados sean internos y busques un contrato tipado, compacto y de baja latencia. Los clientes generados por gRPC dificultan que el código se desvíe del esquema, razón por la cual muchas mallas de servicios internas lo estandarizan.

Las llamadas síncronas son fáciles de razonar, pero es imposible hacer que sean totalmente seguras. Ahora, dos servicios están temporalmente acoplados: el llamador no puede avanzar a menos que el llamado esté activo y responda. Tres reglas evitan que esto se convierta en una caída del sistema.

  • Establece siempre un timeout. Una llamada sin fecha límite puede quedar colgada hasta que tus propios recursos se agoten. Elige un presupuesto que se ajuste a la fecha límite del llamador y resta el resto de su trabajo.
  • Reintenta solo operaciones idempotentes. Un GET reintentado es inofensivo; un POST /charge reintentado es un segundo cargo, a menos que el endpoint acepte y respete una clave de idempotencia.
  • Corta el circuito. Cuando una dependencia está fallando, deja de llamarla por un tiempo en lugar de encolar solicitudes que también fallarán. Esto es el circuit breaker, y es lo que convierte una dependencia lenta en un error rápido y honesto.

Los reintentos amplifican la carga. Si tres capas reintentan tres veces cada una, un servicio con dificultades recibirá veintisiete solicitudes por cada original. Limita los reintentos en el borde (edge), donde puedes ver el panorama completo, y añade jitter para que los reintentos no lleguen en una ola sincronizada.

Comunicación asíncrona: eventos

La alternativa a preguntar es anunciar. Un servicio publica un hecho — orders.created, payment.captured — y cualquier número de consumidores reacciona sin que el productor sepa que existen. Esto elimina el acoplamiento temporal: el servicio de pagos puede estar caído y el pedido se crea de todos modos, ya que el evento espera en el broker.

La comunicación asíncrona aporta resiliencia y flexibilidad a costa de la inmediatez y la claridad. La respuesta del productor ya no incluye el efecto derivado, por lo que el sistema es eventualmente consistente. Rastrear una solicitud implica seguir un rastro de eventos en lugar de un único call stack. Además, el broker se convierte en una infraestructura crítica que debe ser duradera y monitoreada.

Existen dos formas de coordinar un proceso de varios pasos. En la coreografía, cada servicio escucha eventos y decide qué hacer a continuación; no hay un cerebro central, lo cual es elegante hasta que nadie puede explicar cuál es el flujo general. En la orquestación, un gestor de procesos o un coordinador de saga dirige explícitamente los pasos. La coreografía es mejor para reacciones simples, y la orquestación para flujos con compensaciones y timeouts. Las guías de Event-Driven Architecture y Kafka profundizan más en la mecánica.

El impuesto de los sistemas distribuidos

En el momento en que una llamada cruza una red, se vuelve tu responsabilidad gestionar un conjunto de modos de fallo que no existen en un proceso local. Esto no es una razón para evitar los servicios; es la factura que viene incluida con ellos.

  • La red no es fiable. Los paquetes se pierden, el DNS devuelve direcciones obsoletas y las conexiones se reinician a mitad de una solicitud.
  • El fallo es parcial. El Servicio A puede estar saludable mientras que el B está caído. No existe un único indicador de “el sistema está activo”.
  • La latencia no es gratuita. Una llamada que tomaba microsegundos en proceso ahora toma milisegundos, y las cadenas de llamadas multiplican este efecto.
  • La respuesta puede perderse. Una solicitud puede tener éxito pero el acuse de recibo nunca llega. Desde la perspectiva del emisor, falló; sin embargo, el trabajo se realizó.
  • El tiempo no está sincronizado. Los relojes entre máquinas divergen, por lo que ordenar eventos basándose únicamente en el timestamp es inseguro.

La respuesta de diseño es un conjunto de herramientas pequeño y bien conocido: timeouts en cada llamada, reintentos limitados con backoff y jitter, circuit breakers, bulkheads que aíslan el fallo de una dependencia e idempotency keys para que una solicitud repetida sea segura. Ninguno de estos elementos es opcional. Un sistema de microservicios sin ellos es un sistema que funciona hasta que llega el primer día difícil.

Propiedad de los datos y la trampa de la base de datos compartida

Cada servicio es dueño de sus datos. Esto significa que un solo servicio es el único encargado de escribir en su esquema, y ningún otro servicio se conecta a esa base de datos. La propiedad es lo que hace posible el despliegue independiente, ya que un cambio en el esquema afecta únicamente al servicio propietario y a su contrato.

Una base de datos compartida parece conveniente, pero destruye silenciosamente la arquitectura. Dos servicios que leen las mismas tablas están acoplados a través del esquema: si renombras una columna, ambos deben desplegarse. Peor aún, el esquema compartido se convierte en un modelo compartido de facto, y los bounded contexts colapsan nuevamente en uno solo.

Las consultas entre servicios son la razón habitual por la que los equipos recurren a la base de datos compartida. Las soluciones son los read models y las API:

  • Solicita al servicio propietario, a través de su API, una respuesta pequeña y diseñada para un propósito específico.
  • Suscríbete a sus eventos y mantén una projection local adaptada a tus consultas.
  • Acepta que algunas consultas requieren un almacén de reportes dedicado, no un join entre bases de datos de diferentes servicios.

Una projection está desnormalizada y es eventualmente consistente, que es precisamente la razón por la que es rápida y por la que debes sentirte cómodo con un breve retraso (lag). Este es el lado de lectura de CQRS, y es la forma estándar de responder a preguntas entre servicios sin romper la propiedad de los datos.

Sagas en lugar de transacciones distribuidas

No existen las transacciones ACID que abarquen múltiples servicios. El commit de dos fases (two-phase commit) requiere que todos los participantes mantengan bloqueos y estén disponibles, que es precisamente la condición que no se puede garantizar a través de una red, y la mayoría de los datastores modernos no lo soportan. El reemplazo es la saga.

Una saga es una secuencia de transacciones locales, una por servicio, donde cada paso publica un evento o invoca al siguiente. Si un paso posterior falla, la saga ejecuta acciones compensatorias para deshacer el trabajo completado en términos de negocio. Consideremos el proceso de realizar un pedido:

  1. Orders crea el pedido como pending.
  2. Inventory reserva el stock.
  3. Payments realiza el cargo al cliente.
  4. Fulfilment programa el envío y orders marca el pedido como confirmed.

Si el pago falla, la compensación libera la reserva y cancela el pedido. Ten en cuenta que “deshacer” no es un rollback; es un nuevo hecho de negocio. No puedes “des-enviar” un correo electrónico de confirmación, por lo que la compensación es un segundo correo explicando la cancelación.

Debido a que cada paso puede reintentarse, cada paso debe ser idempotente. El mismo pedido no debe cobrarse dos veces. Deriva una clave de deduplicación de la operación de negocio —el ID del pedido— y no de un ID de solicitud aleatorio, y deja que una restricción de unicidad o un SET NX haga que la comprobación sea atómica. Las sagas son la parte más exigente de los microservicios, y omitir la ruta de compensación es la razón por la cual los sistemas terminan con reservas huérfanas y cargos duplicados.

Contratos, versionado y compatibilidad

El contrato de un servicio es su API, y quienes lo consumen dependen de él. Trátalo como una interfaz publicada con un ciclo de vida:

  • Añade, no rompas. Los nuevos campos opcionales son seguros; renombrar, eliminar o cambiar el significado de un campo no lo es.
  • Versiona deliberadamente. El versionado mediante URL o encabezados hace que los cambios disruptivos sean explícitos; las guías de REST y de versionado de API cubren las ventajas y desventajas de cada enfoque.
  • Da tiempo a los consumidores. Anuncia las deprecaciones, emite métricas de uso por cliente y elimina un endpoint solo cuando ya nadie lo esté llamando.
  • Prueba el contrato desde ambos lados. Las pruebas de contrato impulsadas por el consumidor (consumer-driven contract tests) detectan los casos en los que un cambio “compatible” del productor no es compatible con un consumidor en particular.

La misma disciplina se aplica a los esquemas de eventos. Un consumidor que lea un topic verá mensajes escritos por una versión anterior del productor, por lo que los eventos deben ser aditivos y autodescriptivos. Incluye una versión o un id de esquema en el sobre (envelope) y permite que los consumidores toleren los campos desconocidos.

Descubrimiento, gateways y balanceo de carga

Los servicios se mueven. Las instancias se reprograman, se escalan y se reemplazan, por lo que quienes realizan las llamadas no pueden hard-codear las direcciones. El service discovery resuelve esto: ya sea que el cliente consulte a un registro para encontrar instancias saludables (client-side), o que una dirección virtual estable frente a las instancias se encargue de ello (server-side). Kubernetes te ofrece esto último de forma gratuita a través de Services y DNS.

Un API gateway se sitúa en el borde y gestiona las responsabilidades que, de otro modo, cada servicio tendría que repetir: autenticación, rate limiting, terminación TLS, enrutamiento de solicitudes y agregación de respuestas. Es genuinamente útil. Sin embargo, también se convierte en un punto único de fallo y, si acumula lógica de negocio, en un monolito distribuido en miniatura. Mantén el gateway ligero y delega las reglas específicas de cada capacidad al servicio propietario.

El balanceo de carga no es solo round-robin. Las conexiones de larga duración, las sesiones persistentes (sticky sessions) y los consumidores lentos afectan la uniformidad con la que se distribuye el tráfico. Deja que el balanceador de carga de la plataforma haga su trabajo y haz que los servicios sean stateless para que cualquier instancia pueda atender cualquier solicitud.

Observabilidad entre servicios

En un monolito, un stack trace suele ser suficiente para explicar un incidente. Entre servicios, la solicitud abandonó tu proceso hace varios saltos y el rastro se ha perdido. Tres instrumentos permiten recuperarlo.

  • Correlation ids. Genera un id en el edge, propágalo en los headers y en los envelopes de los eventos, e inclúyelo en cada línea de log. Es el hilo que vincula la acción de un usuario con cada servicio que haya tocado.
  • Distributed tracing. Los spans de OpenTelemetry registran el tiempo transcurrido en cada servicio y llamada. Un trace muestra de un vistazo qué salto es lento, que es la pregunta que los logs no pueden responder.
  • Métricas por servicio. Rastrea la tasa de solicitudes, la tasa de errores y la duración de cada endpoint, además de señales de saturación como la profundidad de la cola y el uso del pool. Configura alertas basadas en los síntomas que sienten tus usuarios, no en cada pequeña fluctuación.

Los logs deben ser JSON estructurados con un nombre de servicio y el correlation id, enviados a un único lugar. Un log que no puedes buscar a través de los servicios es un log que no utilizarás a las 3 de la mañana. La guía de Cloud Deployment cubre la parte operativa de la recolección de estas señales.

Deadlines y presupuestos de solicitud

La latencia en una cadena de llamadas es aditiva, y cada servicio en la cadena está adivinando a menos que propagues un deadline. Una solicitud orientada al usuario que debe responder en 800ms no puede permitirse cuatro llamadas downstream que estén dispuestas a esperar dos segundos cada una.

Asigna un presupuesto (budget) a cada solicitud en el edge y pasa el tiempo restante a través de la llamada. Cada servicio resta el tiempo de su propio trabajo y reenvía un deadline más corto, de modo que la cadena falle rápido en lugar de acumular timeouts.

// The gateway sets the budget once.
const deadline = Date.now() + 800;

await orders.place(input, { deadline }); // forwards the remaining time

Cuando un servicio detecta que el deadline ha expirado, debe detenerse y devolver un error honesto en lugar de iniciar más trabajo que no podrá terminar. Combina esto con un fallback para llamadas no esenciales: si el servicio de recomendaciones no puede responder en 100ms, devuelve la página sin ellas. Un presupuesto de solicitud convierte el “a veces se queda colgado” en un p99 predecible, y es una de las victorias de fiabilidad más económicas disponibles entre servicios.

Despliegue e infraestructura

El despliegue independiente es una propiedad de tu pipeline, no solo de tu topología. Cada servicio necesita su propio proceso de build, test, imagen y rollout, además de una forma de ejecutar migraciones de base de datos sin tiempo de inactividad y un rollback que no dependa de volver a desplegar todo lo demás.

Esto generalmente implica el uso de contenedores y un orquestador. Los contenedores hacen que el runtime del servicio sea portable y que sus dependencias sean explícitas; Kubernetes o un equivalente gestionado se encarga del scheduling, discovery, health checks, autoscaling y rolling updates. También necesitas que la configuración y los secrets se entreguen por entorno, y una estrategia para las migraciones de esquema que mantenga la compatibilidad con la versión anterior del código, ya que las instancias antiguas y nuevas se ejecutan en paralelo durante un rollout.

Este es el “impuesto de plataforma”. Es real, es constante y es la razón por la cual los equipos pequeños deberían pensarlo bien antes de adoptar este estilo.

El monolito distribuido

El monolito distribuido es el peor escenario posible: muchos servicios que, aun así, deben desplegarse juntos. Sus síntomas son inconfundibles.

  • Los servicios comparten una base de datos, por lo que los cambios de esquema afectan a múltiples equipos.
  • Un lanzamiento requiere coordinar varios servicios a la vez.
  • Una cadena de llamadas síncronas atraviesa cuatro servicios para una sola acción del usuario.
  • Dos servicios siempre se modifican en el mismo pull request.

Cuando observes esto, significa que los límites están mal definidos. Las causas habituales son dividir por capas técnicas, dividir antes de comprender el dominio o permitir que una base de datos compartida sobreviva a la división. La solución suele ser fusionar nuevamente los servicios problemáticos y buscar un punto de corte más adecuado; es una admisión difícil, pero sale más barata que una década de lanzamientos coordinados.

Cuándo no usar microservicios

No dividas la aplicación si se cumple cualquiera de estos puntos:

  • El equipo es lo suficientemente pequeño como para que todos puedan desplegar la aplicación completa de forma segura.
  • El dominio aún se está descubriendo, por lo que los límites serían meras suposiciones.
  • El producto está en una etapa temprana y los requisitos cambian semanalmente.
  • Aún no cuentas con la capacidad de CI/CD, observabilidad y guardias (on-call) para gestionar múltiples servicios.

En todos estos casos, un monolito modular te ofrece la misma disciplina interna pero con un único despliegue, transacciones en el mismo proceso y refactorizaciones que no requieren un plan de migración. Debido a que sus módulos se comunican a través de interfaces explícitas, un módulo puede extraerse en un servicio más adelante, cuando surja una razón concreta: un equipo que necesite su propio ritmo de entrega, un componente con un perfil de escalado genuinamente diferente o un límite de cumplimiento normativo. Extraer un módulo limpio es mucho más económico que fusionar una división mal ejecutada.

Migrando con el patrón strangler fig

Cuando decidas dividir un sistema existente, hazlo de manera incremental. El patrón strangler fig redirige el tráfico a través de una fachada y traslada una funcionalidad a la vez hasta que el código antiguo deja de utilizarse y puede eliminarse.

  1. Encuentra una costura (seam). Elige una funcionalidad que ya sea bastante autónoma y que cambie con frecuencia. El mejor candidato es un módulo con límites bien definidos.
  2. Implementa una fachada. Redirige el tráfico relevante a través de un proxy o gateway para que puedas desplazarlo sin afectar a los clientes.
  3. Extrae el módulo a un servicio. Asígnale su propio despliegue y pipeline, y mueve sus tablas a una base de datos propia.
  4. Sincroniza los datos con cuidado. Implementa dual-write o publica eventos y realiza un backfill hasta que el nuevo almacenamiento sea la fuente de verdad; entonces, deja de escribir en las tablas antiguas.
  5. Realiza el corte y elimina. Desvía el tráfico, monitorea las métricas y elimina la ruta del código antiguo una vez que ya no reciba peticiones.

Nunca empieces con una reescritura total (big-bang rewrite). El valor del enfoque strangler es que cada paso es reversible y el sistema permanece en producción durante todo el proceso.

Definir primero el contrato

Un contrato HTTP interno es propenso a desincronizarse fácilmente, ya que el productor y el consumidor suelen ser escritos por equipos diferentes y solo las pruebas de integración detectan los errores. Definir el contrato como un esquema antes de escribir cualquiera de las dos partes garantiza la coherencia y permite generar clientes, servidores y documentación a partir de una única fuente.

// inventory.proto
service Inventory {
  rpc GetStock(GetStockRequest) returns (StockLevel);
  rpc Reserve(ReserveRequest) returns (Reservation);
}

message GetStockRequest { string sku = 1; }
message StockLevel { string sku = 1; int32 available = 2; }

Para HTTP, un documento OpenAPI cumple la misma función. En cualquier caso, el esquema es el artefacto sujeto a revisión, y un cambio disruptivo (breaking change) aparece en un diff en lugar de en producción. Las pruebas de contrato impulsadas por el consumidor (consumer-driven contract tests) completan el ciclo al codificar lo que cada consumidor utiliza realmente, permitiendo que el productor sepa, antes del lanzamiento, si su cambio es seguro para los clientes existentes.

Emparejando el transporte con la interacción

El error común es utilizar un único transporte para todo. Un sistema puramente REST serializa cada reacción detrás de una cadena de llamadas; un sistema puramente basado en eventos hace que una simple consulta sea absurdamente indirecta. Elige según la interacción, no según el sistema.

Interacción Transporte Por qué
Navegador al backend REST/JSON Ubicuo, cacheable, fácil de depurar
Servicio a servicio, baja latencia gRPC Tipado, compacto, clientes generados
Reacción de tipo “dispara y olvida” Events Sin acoplamiento temporal, con búfer
Flujo de trabajo de larga duración Events más una saga Duradero, reintentable, compensado
Lectura de alto volumen Cache o modelo de lectura Evita la llamada por completo

Un estándar útil es hacer que las escrituras sean autoritativas mediante una llamada síncrona cuando el llamador necesita la respuesta, y publicar un evento para cada efecto que el llamador no necesite esperar.

Claves de idempotencia en la práctica

Las Sagas, los reintentos y los eventos de entrega “al menos una vez” (at-least-once) implican que una misma operación puede llegar dos veces. La única defensa que sobrevive a la concurrencia es una verificación de deduplicación que sea atómica con el efecto secundario.

export async function capturePayment(cmd: CapturePayment) {
  const key = `payment:captured:${cmd.orderId}`;
  const inserted = await redis.set(key, "1", "NX", "EX", 86_400);
  if (inserted === null) return { skipped: true };

  return gateway.charge(
    { orderId: cmd.orderId, amountCents: cmd.amountCents },
    { idempotencyKey: cmd.orderId },
  );
}

Hay tres detalles fundamentales. La clave se deriva de la operación de negocio, no de un ID de solicitud aleatorio, de modo que un productor que encole el mismo trabajo lógico dos veces siga generando una colisión. La verificación y la escritura del marcador son una única operación atómica, para que dos trabajadores concurrentes no puedan ganar ambos. Y al proveedor se le asigna su propia clave de idempotencia, ya que tu marcador puede perderse mientras que el registro del proveedor sobrevive.

Outbox, inbox y efectos exactly-once

Un servicio que escribe en su base de datos y luego publica un evento tiene una vulnerabilidad: el proceso puede fallar entre ambas acciones, dejando el estado modificado pero el evento sin enviar. El patrón outbox soluciona esto escribiendo el evento en una tabla outbox dentro de la misma transacción local que el cambio de estado, permitiendo luego que un relay publique las filas.

await db.transaction(async (tx) => {
  await orders.save(order, tx);
  await tx.insert(outbox).values({
    id: randomUUID(),
    topic: "orders.created",
    key: order.id,
    payload: order.toEvent(),
  });
});

// A separate relay polls outbox and publishes, at least once.

El lado del consumidor es la imagen especular. Debido a que la entrega es at-least-once, un consumidor puede recibir el mismo evento dos veces. Mantén una tabla inbox con los IDs de los eventos procesados y omite los duplicados, nuevamente dentro de la misma transacción que el efecto. El uso conjunto de outbox e inbox permite un procesamiento effectively-once sin pretender que la red sea fiable.

Bulkheads y degradación gradual

Un circuit breaker deja de llamar a una dependencia que está fallando. Un bulkhead va más allá e aisla los recursos para que una sola dependencia no pueda consumirlo todo. Si el servicio de recomendaciones tiene su propio pool de conexiones, un bloqueo allí no podrá agotar el pool del que depende el proceso de checkout.

const pools = {
  checkout: new Pool({ max: 20 }),
  recommendations: new Pool({ max: 5, timeoutMs: 500 }),
};

La degradación es la parte orientada al usuario de esta misma idea. Cuando una dependencia no esencial es lenta, devuelve una respuesta reducida en lugar de un error. Una página de producto sin recomendaciones personalizadas sigue siendo una buena página; una página de producto que da un timeout porque las recomendaciones fallaron es una caída del servicio. Decide para cada llamada si es obligatoria o de “mejor esfuerzo” (best-effort), y codifica esa decisión en el lugar donde se realiza la llamada.

La capa anticorrupción (anti-corruption layer)

Cuando un servicio consume el modelo de otro directamente, hereda las suposiciones de dicho modelo, y cualquier cambio en la forma del Order del productor repercute en cada consumidor. Una capa anticorrupción es una capa de traducción ligera que mapea el contrato externo al lenguaje de dominio propio del consumidor.

// Shipping has its own idea of a shipment. It translates the event.
function toShipment(event: OrderCreated): ShipmentRequest {
  return {
    reference: event.id,
    destination: event.shippingAddress,
    lines: event.lines.map((l) => ({ sku: l.sku, quantity: l.qty })),
  };
}

Esta capa requiere escribir un poco más de código, pero a cambio otorga una independencia real. El consumidor puede renombrar sus propios conceptos libremente, tolerar campos desconocidos y absorber cambios disruptivos (breaking changes) de un tercero que no controla.

Cómo saber si la división está funcionando

Los microservicios son un medio, no un objetivo, así que mide lo que realmente querías lograr. Las señales que indican que la división está dando frutos son organizacionales: la cantidad de equipos que pueden hacer despliegues sin coordinarse, el lead time desde el commit hasta producción para un único servicio, y con qué frecuencia un solo cambio afecta a varios servicios. Si esos números no mejoran, la topología no está justificando su costo.

Las señales que sugieren que las cosas van mal son técnicas y fáciles de detectar:

  • Los despliegues siguen ocurriendo en un orden fijo entre servicios.
  • Una sola acción del usuario genera una larga cadena de llamadas síncronas.
  • Un cambio en el esquema de un servicio requiere el lanzamiento de otro equipo.
  • El equipo de guardia no puede determinar qué servicio causó un incidente.

Cualquiera de estos puntos significa que tienes un monolito distribuido, y la solución suele ser fusionar un límite, no añadir otro servicio.

Una lista de verificación antes de dividir

Antes de extraer un módulo de un monolito, asegúrate de que todo lo siguiente sea cierto.

  • El módulo ya es dueño de sus datos y nadie más escribe en sus tablas.
  • Los demás módulos dependen de su interfaz, no de sus componentes internos.
  • Tienes una razón más allá de que “se siente demasiado grande”: el ritmo de un equipo, un perfil de escalado o un límite de cumplimiento (compliance).
  • Tienes CI/CD, tracing y capacidad de guardia (on-call) para un despliegue más.
  • Tienes un plan para la migración de datos y una forma de revertirla.
  • Has decidido cómo se comunicará el módulo con el resto, ya sea de forma síncrona o asíncrona, y has redactado el contrato.

Si alguna respuesta es negativa, la extracción será más costosa de lo que parece. Corregir el límite dentro del monolito es casi siempre más barato que corregirlo a través de una red.

Mejores prácticas

  • Divide los servicios para lograr un despliegue independiente y autonomía del equipo, no por escalabilidad.
  • Alinea cada servicio con un bounded context y asígnale un responsable claro.
  • Asigna a cada servicio su propia base de datos y prohíbe las consultas entre servicios.
  • Configura timeouts, reintentos limitados con jitter y circuit breakers en cada llamada.
  • Haz que las operaciones de escritura sean idempotentes antes de permitir cualquier reintento.
  • Prefiere el uso de eventos para reacciones que no requieran una respuesta inmediata.
  • Utiliza sagas con compensaciones en lugar de transacciones distribuidas.
  • Versiona los contratos y asegúrate de que cada cambio sea aditivo.
  • Propaga un correlation id e implementa tracing desde el primer servicio.
  • Mantén el gateway ligero y desplaza la lógica de negocio hacia los servicios.
  • Extrae los servicios utilizando el patrón strangler fig, una capacidad a la vez.

Errores comunes

  • Dividir por capas técnicas y terminar necesitando tres servicios para cada funcionalidad.
  • Compartir una base de datos y preguntarse por qué los despliegues aún requieren coordinación.
  • Desplegar servicios en conjunto y llamar al resultado microservicios.
  • Añadir reintentos sin idempotencia y cobrar dos veces a los clientes.
  • No configurar los timeouts, provocando que una sola dependencia lenta bloquee toda la cadena de llamadas.
  • Usar cadenas síncronas donde un evento eliminaría el acoplamiento.
  • Construir una saga sin una ruta de compensación para los casos de fallo.
  • Omitir el trazado distribuido y depurar basándose en suposiciones al revisar los logs.
  • Dividir demasiado pronto, congelando límites incorrectos en una infraestructura costosa.
  • Tratar a Kubernetes y a un service mesh como el objetivo en lugar del costo para alcanzar dicho objetivo.

Próximos pasos

Si sientes que las desventajas mencionadas son demasiado costosas para tu equipo, lee la guía sobre Modular Monolith; es la opción predeterminada ideal y la mejor preparación para una futura división. Para lograr que los servicios se comuniquen sin llamarse entre sí, el siguiente paso es la guía de Event-Driven Architecture, utilizando Kafka para el log subyacente. Y una vez que tengas muchos servicios en ejecución, la guía de Cloud Deployment cubre el trabajo de plataforma y observabilidad necesario para mantenerlos saludables.

En la practica

Contrato, llamada, evento, disponibilidad

Los cuatro artefactos que produce cada servicio en un sistema saludable.

services/orders/http.ts
import Fastify from "fastify";
import { z } from "zod";

const app = Fastify();

const CreateOrder = z.object({
  customerId: z.string().uuid(),
  lines: z
    .array(z.object({ sku: z.string(), qty: z.number().int().positive() }))
    .min(1),
});

app.post("/orders", async (req, reply) => {
  const body = CreateOrder.parse(req.body);
  const order = await orders.create(body);
  reply.code(201).send({ id: order.id, status: order.status });
});

Eventos frente a llamadas síncronas

Una llamada dice "ahora" y espera. Un evento dice "esto sucedió" y deja que el otro lado reaccione. Elige por interacción, no por sistema.

Preferir eventos
// The order service does not know or care who reacts.
await events.publish("orders.created", { key: order.id, value: order });

// Analytics, email and fulfilment subscribe independently.
Evitar cadenas de llamadas
// Every new reaction edits this function and adds a hop
// that can fail before the response is returned.
await analytics.record(order);
await email.sendReceipt(order);
await fulfilment.reserve(order);

Una base de datos por servicio

Una base de datos compartida es la forma más rápida de perder la capacidad de despliegue independiente. Un cambio de esquema requeriría entonces que todos los equipos propietarios lancen cambios juntos.

Preferir
// orders owns orders_db. Nobody else connects to it.
const order = await ordersDb.insert(orders).values(input);

await events.publish("orders.created", { key: order.id, value: order });
Evitar
// shipping reaches into the orders tables directly.
// Now a column rename is a cross-team release.
const rows = await db.query(
  "SELECT id, status FROM orders WHERE customer_id = $1",
  [customerId],
);

Compromisos

¿Deberías dividir esto en servicios?

Los microservicios cambian la simplicidad de los procesos internos por la complejidad operativa y de coordinación. Solo algunas organizaciones obtienen un retorno de ese intercambio.

Strengths

  • Los equipos lanzan sin esperar

    Un servicio propiedad de un equipo puede desplegarse según su propio calendario. El tren de lanzamientos desaparece, y con él la mayor parte de la coordinación entre equipos.

  • Los fallos se mantienen contenidos

    Un fallo en las recomendaciones no tumba el proceso de pago. Los bulkheads, los despliegues separados y el escalado independiente limitan el radio de impacto.

  • Cada parte escala en su propia curva

    El servicio de búsqueda puede ejecutarse en muchos nodos con alta carga de CPU mientras que el de facturación corre en dos pequeños, en lugar de escalar toda la aplicación junta.

Trade-offs

  • La red se convierte en tu problema

    Cada llamada puede ser lenta, puede expirar o puede tener éxito mientras la respuesta se pierde. Los timeouts, reintentos e idempotencia son ahora preocupaciones de la aplicación.

  • La consistencia se vuelve más difícil

    No hay transacciones a través de servicios. Necesitas sagas, acciones compensatorias y consistencia eventual en lugar de un único rollback.

  • La factura de la plataforma es real

    Contenedores, orquestación, registros, trazado, gateways y guardias para muchos servicios son costos que un monolito simplemente no tiene.

  • Los límites incorrectos son costosos

    Si divides antes de que el dominio esté claro, obtienes un monolito distribuido donde cada cambio sigue requiriendo un lanzamiento coordinado.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender Microservices?

Nuestro tutorial interactivo te guia a traves de Microservices paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.