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
GETreintentado es inofensivo; unPOST /chargereintentado 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:
- Orders crea el pedido como
pending. - Inventory reserva el stock.
- Payments realiza el cargo al cliente.
- 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.
- 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.
- 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.
- Extrae el módulo a un servicio. Asígnale su propio despliegue y pipeline, y mueve sus tablas a una base de datos propia.
- 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.
- 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.