desarrollo de software20 min de lectura

Modular Monolith Architecture. cuando menos piezas dan más control

Por Sergio Perea #arquitectura#software#monolito#microservicios#modular monolith#estrategia tecnológica
Modular Monolith Architecture. cuando menos piezas dan más control

La intención de este artículo es informacional: entender por qué muchas empresas están dando marcha atrás con los microservicios y qué criterios prácticos pueden ayudarte a decidir entre seguir invirtiendo en microservicios o consolidar hacia un monolito modular sin perder orden ni calidad técnica.

El debate real no va de elegir “la arquitectura correcta” como si fuese una receta universal. En la práctica, casi nunca es microservicios vs monolito. En realidad el problema que aparece una y otra vez en los proyectos IT es este:

  • Equipos con capacidad finita: no todos los equipos tienen personas dedicadas a operaciones, observabilidad o plataformas. Cada nuevo servicio añade carga mental y trabajo invisible.
  • Costes cloud que crecen sin impacto claro en el cliente: más servicios implica más tráfico interno, más entornos, más monitorización y más piezas siempre encendidas, aunque el producto no venda más.
  • Depuración lenta por fallos distribuidos: cuando un flujo atraviesa varios servicios, encontrar dónde se rompe algo deja de ser obvio. El error no está “en un sitio”, está repartido.
  • Autonomía mal entendida: desplegar de forma independiente no garantiza independencia real. Si un cambio pequeño obliga a coordinar a varios equipos, la autonomía es solo aparente.

Este patrón se repite mucho en sistemas reales. Se adopta microservicios buscando velocidad, escalabilidad y libertad, y el resultado acaba siendo otro: un sistema más difícil de entender, más caro de mantener y con equipos dedicando más tiempo a sostener la arquitectura que a mejorar el producto.

Se compra la promesa de escalado “infinito”, pero lo que llega primero es complejidad, guardias más duras, latencia innecesaria y menos foco en lo que realmente importa: entregar valor de forma constante y predecible.

El día que tu arquitectura te quita horas de vida

Imagina que tu equipo tarda dos días en localizar un bug. No porque el bug sea especialmente complejo, sino por todo lo que hay alrededor: seguir una traza que salta de servicio en servicio, distinguir un timeout real de un retry mal configurado, revisar colas que procesan el mensaje dos veces, comprobar si la versión desplegada en staging es la misma que en producción. El fallo no está escondido. Está disperso.

Y ese tiempo no lo paga el servidor. Lo paga tu roadmap.

Cada hora invertida en entender el sistema es una hora que no va a una funcionalidad, a una mejora de rendimiento o a una corrección visible para el usuario. Y cuando eso ocurre de forma repetida, la arquitectura deja de ser una ayuda y pasa a ser un freno silencioso.

En negocio se nota así:

  • Más coste: necesitas más observabilidad, más tráfico interno entre servicios, sidecars por todas partes y entornos duplicados para probar cambios que antes eran triviales.
  • Menos entrega: el equipo dedica buena parte del tiempo a mantener viva la plataforma en lugar de evolucionar el producto. Se trabaja mucho, pero se avanza poco.
  • Más rotación: guardias duras, alertas que no siempre apuntan al origen del problema y la sensación constante de apagar fuegos sin cerrar nunca el incendio.
  • Más riesgo: cambios pequeños que cruzan varios contratos, rompen integraciones y obligan a desplegar con miedo, aunque el cambio sea menor.

Lo interesante es que el sector está corrigiendo este exceso. No desde el discurso, sino desde los datos. Un informe reciente de CNCF sobre el estado del ecosistema cloud native muestra una caída clara en la adopción de service mesh: del 18% en Q3 de 2023 al 8% en Q3 de 2025. ([CNCF][1])

No es un juicio moral ni una moda nueva. Es una señal clara. Muchas organizaciones probaron el “stack completo”, lo llevaron a producción y descubrieron que, para su tamaño y contexto, el coste cognitivo y operativo era demasiado alto. La respuesta no ha sido volver al caos, sino recortar complejidad, quedarse con lo que aporta valor real y repensar dónde merece la pena pagar ese precio.


Capa estratégica: por qué está pasando la microservices consolidation

1) El coste de microservicios no es solo cloud

El coste grande suele ser humano:

  • Más coordinación entre equipos.
  • Más surface area: contratos, versiones, despliegues, permisos.
  • Más sitios donde mirar: logs, métricas, traces, colas, DLQ.

A cierta escala, con equipos grandes, dominios bien separados y una capacidad operativa madura, los microservicios pueden ser el movimiento correcto. Cuando hay necesidad real de aislar despliegues, escalar partes muy concretas del sistema o permitir ritmos de trabajo totalmente independientes, el coste adicional tiene sentido y se amortiza.

El problema aparece cuando ese mismo enfoque se replica en contextos donde el “premium” arquitectónico no se recupera. Equipos pequeños o medianos adoptan microservicios antes de que existan las tensiones que los justifican. Se paga complejidad por adelantado esperando beneficios futuros que nunca llegan.

En esos escenarios, cada servicio nuevo añade más coordinación, más infraestructura y más puntos de fallo, sin resolver un problema real del negocio. La arquitectura se vuelve más cara y más frágil, no porque los microservicios estén mal diseñados, sino porque el contexto todavía no los necesitaba.

Ahí es donde el retorno se rompe: el sistema crece en piezas, pero no en capacidad de entrega. Y cuando el coste de sostener la arquitectura supera al valor que aporta al producto, el problema ya no es técnico. Es estratégico.

2) Autonomía de equipo ≠ “un repositorio por servicio”

La autonomía real es:

  • Un dominio claro.
  • Un ownership claro.
  • Interfaz clara con el resto.
  • Guardrails que eviten acoplamiento.

Eso se puede lograr con microservicios. También con monolito modular si se hace bien.

3) La complejidad de depurar sistemas distribuidos es adictiva y cara

Si tu sistema hace 8 hops por request, te comes:

  • Latencia acumulada.
  • Fallos parciales.
  • Timeouts en cascada.
  • Datos inconsistentes por flujos asíncronos mal gobernados.

En el mundo real, ese “evento raro” acaba pasando cada semana.

4) Los costes cloud ya no se perdonan

Cuando el dinero era barato, se aceptaba una arquitectura “bonita” aunque fuese cara. En 2026 el CFO pregunta cosas simples:

  • ¿Qué gana el cliente con este gasto?
  • ¿Qué pasa si cortamos un 30% de infraestructura?
  • ¿Cuánto tarda un dev nuevo en ser productivo?

Aquí es donde el monolito modular empieza a brillar.


Qué es un monolito modular (y qué no es)

Un modular monolith es una aplicación que se despliega como una sola unidad —o en muy pocas— pero que está dividida internamente en módulos claros, cada uno con una responsabilidad concreta dentro del negocio. No se trata de juntar todo “porque sí”, sino de diseñar límites internos tan estrictos como si fuesen servicios independientes, solo que sin el coste de red y de operación que implica separarlos físicamente.

En la práctica, un monolito modular implica:

  • Un solo despliegue (o pocos): hay una versión del sistema, una pipeline principal y un flujo de despliegue fácil de razonar. Menos sincronización, menos estados intermedios.
  • Módulos internos con límites fuertes: cada módulo representa una capacidad de negocio clara. Lo que ocurre dentro del módulo es responsabilidad de su equipo; lo que sale, lo hace a través de una interfaz definida.
  • Dependencias controladas: no todo puede llamar a todo. Las reglas de dependencia se definen y se cumplen. Si un módulo necesita otro, lo hace de forma explícita y consciente.
  • Contratos claros entre módulos: las interacciones no se basan en “importar una clase y ya está”, sino en APIs internas bien definidas, estables y pensadas para evolucionar sin romper el resto del sistema.

Un monolito modular no es:

  • “Todo en una carpeta /src y que Dios reparta suerte”. Eso no es un monolito modular, es deuda técnica esperando a crecer.
  • Un “microservicio dentro del monolito” que se salta reglas cuando hay prisa. Si los límites no se respetan bajo presión, los límites no existen.
  • Compartir base de datos entre pseudo-servicios sin ninguna disciplina. Ese enfoque suele acabar en lo peor de los dos mundos: ni simplicidad operativa ni separación real. Es el clásico monolito distribuido.

La idea clave es sencilla, pero cuesta interiorizarla: el límite lógico importa más que el límite de despliegue. Si los módulos están bien definidos y protegidos, el sistema puede crecer de forma ordenada. Si no lo están, separar en servicios solo mueve el problema de sitio y lo hace más caro.

Un buen monolito modular no debería ser un paso atrás. Es una forma consciente de comprar orden, foco y previsibilidad, pagando solo la complejidad que el contexto realmente necesita.


Impacto técnico vs impacto de negocio

Lo que cambia en técnico

  • Menos red interna.
  • Trazas más sencillas.
  • Transacciones locales más fáciles.
  • Testing de integración más barato.
  • Build más pesado si no cuidas el tooling.

Lo que cambia en negocio

  • Baja MTTR (tiempo medio de reparación) si el código está bien particionado.
  • Baja el gasto en infraestructura “interna”.
  • Sube la previsibilidad de entrega.
  • Sube la capacidad de onboarding.

Si tu equipo siente que “vivís dentro del Grafana”, suele ser una pista.


Capa técnica: cómo se construye un monolito modular con límites de verdad

Voy a usar TypeScript para que sea fácil de leer. La idea aplica igual en Java, Kotlin, C# o Python.

1) Estructura por módulos (no por capas genéricas)

// /src // /modules // /billing // /application // /domain // /infrastructure // index.ts // /orders // /identity // /shared // /kernel // /messaging

Regla mental: si alguien cambia /modules/billing/*, debería poder razonar sobre el impacto sin abrir 40 carpetas.

2) Contratos: puertos hacia fuera, dominio hacia dentro

// modules/billing/domain/Invoice.ts export type InvoiceId = string; export class Invoice { constructor( public readonly id: InvoiceId, public readonly customerId: string, public readonly totalCents: number, ) {} validate() { if (this.totalCents <= 0) throw new Error("Invalid total"); } }
// modules/billing/application/ports/PaymentsGateway.ts export interface PaymentsGateway { charge(input: { customerId: string; amountCents: number; reference: string; }): Promise<{ paymentId: string }>; }
// modules/billing/application/CreateInvoice.ts import { Invoice } from "../domain/Invoice"; import type { PaymentsGateway } from "./ports/PaymentsGateway"; export class CreateInvoice { constructor(private readonly payments: PaymentsGateway) {} async execute(input: { invoiceId: string; customerId: string; totalCents: number; }) { const invoice = new Invoice(input.invoiceId, input.customerId, input.totalCents); invoice.validate(); const payment = await this.payments.charge({ customerId: invoice.customerId, amountCents: invoice.totalCents, reference: invoice.id, }); return { invoiceId: invoice.id, paymentId: payment.paymentId }; } }

Fíjate en el detalle: el caso de uso no sabe si hay Stripe, Adyen o una pasarela casera. Solo conoce el puerto.

3) Enforcing: reglas automáticas o se rompen

Un monolito modular sin enforcement se degrada. Siempre.

Soluciones típicas:

  • Tests de arquitectura (por ejemplo: reglas de imports).
  • Linter con restricciones por ruta.
  • Owners por módulo y revisión obligatoria.

Ejemplo simple con una regla de imports (conceptual):

// pseudo-test: nadie puede importar domain de otro módulo // salvo a través de su "public API" (index.ts) const forbidden = [ { from: "modules/orders/**", to: "modules/billing/domain/**" }, ]; test("architecture boundaries", () => { const violations = scanImports(forbidden); // tu herramienta/ script expect(violations).toEqual([]); });

No es glamour. Es supervivencia.

4) Event-driven dentro del proceso (sin red)

Mucha gente asocia event-driven architecture con Kafka y mil topics. Puedes ganar desacoplo con un bus in-process:

// shared/messaging/EventBus.ts export type DomainEvent = { type: string; payload: unknown }; export class EventBus { private handlers = new Map<string, Array<(e: DomainEvent) => Promise<void>>>(); on(type: string, handler: (e: DomainEvent) => Promise<void>) { const list = this.handlers.get(type) ?? []; list.push(handler); this.handlers.set(type, list); } async publish(event: DomainEvent) { const list = this.handlers.get(event.type) ?? []; await Promise.all(list.map(h => h(event))); } }

Esto te da:

  • Separación por eventos.
  • Menos acoplamiento directo.
  • Cero latencia de red.

Riesgos:

  • Si el handler falla, ¿qué pasa?
  • ¿Hay retry?
  • ¿Hay orden?

Para flujos críticos, igual terminas con cola real. Está bien. El monolito modular no prohíbe colas; prohíbe meter colas por defecto.


Lecciones aprendidas (en primera persona)

Lo que intenté

He vivido equipos que empezaron con 12 microservicios con 8 devs. Se montó Kubernetes, service mesh, tracing, todo “como debe ser”.

Lo que falló

Los bugs se convertían en gymkanas. Nadie quería tocar el flujo de pago. El “time-to-market” se fue. El coste de cloud creció con tráfico interno y entornos replicados.

Lo que funcionó en producción

Consolidar en un monolito modular con límites fuertes y ownership claro. El mismo equipo, menos piezas, más foco. Depurar volvió a ser una tarea de horas, no de días.

Lo que haría distinto hoy

Pondría desde el día uno:

  • Módulos por capacidades de negocio.
  • Reglas automáticas de dependencias.
  • Observabilidad básica, sin festival de tooling.
  • Extracción selectiva solo cuando haya una razón clara (escala, aislamiento, compliance, equipo).

Riesgos reales del monolito modular (para no venderte humo)

  • Build y CI: si tu pipeline es lento, el monolito duele.
  • Deploy coordinado: un despliegue afecta a todo. Necesitas disciplina.
  • “Módulos de mentira”: si no hay enforcement, volverás al barro.
  • Hotspots: un módulo central puede convertirse en cuello de botella de cambios.

Mitigaciones típicas:

  • Cachés, colas, réplicas de lectura.
  • Feature flags y despliegues frecuentes.
  • Propiedad por módulo y contratos estables.

Cuándo NO elegir este enfoque

  • Si tienes equipos grandes con necesidades de despliegue totalmente independientes y ritmos distintos.
  • Si hay requisitos de aislamiento extremo (multi-tenant duro, compliance por unidad).
  • Si un subdominio necesita un stack propio por razones técnicas de verdad.
  • Si ya tienes microservicios sanos, con ownership claro y costes bajo control.

Microservicios no están “mal”. Lo que está mal es pagar el premium sin retorno.


Errores comunes que he visto en producción

  • Llamadas síncronas encadenadas entre módulos como si fuesen servicios (pero dentro del proceso).
  • “Shared” infinito: un /shared que crece y se convierte en dependencia global.
  • Módulos por equipo en vez de por dominio (Conway pega fuerte).
  • Eventos sin contrato, sin versionado, sin gobernanza.

Mini-plan de migración desde microservicios

  1. Mapa de dominios: agrupa servicios por capacidad de negocio.
  2. Elige una primera isla: la que más sufra por latencia, coste o depuración.
  3. Strangler en el perímetro: ruta una parte del tráfico al módulo nuevo.
  4. Consolida datos con cuidado: primero lógica, luego almacenes si hace falta.
  5. Mide: coste cloud, MTTR, lead time, errores. Sin métricas, hay fe.

Si necesitas munición para dirección, hay casos públicos que ayudan. Prime Video reportó ahorros fuertes al consolidar un sistema de monitorización en un servicio unificado. ([Coding Interviews Made Simple][2])


¿Qué beneficios tiene un monolito modular frente a microservicios?

Menos complejidad operativa, depuración más directa, menos latencia interna y un coste cloud menor en tráfico y tooling. Mantienes disciplina por módulos si impones reglas.

¿La modular monolith architecture sirve para empresas grandes?

Sí. Shopify ha explicado públicamente el tamaño y retos de su monolito, con millones de líneas de Ruby, y cómo han tenido que invertir en límites y tooling. ([Shopify][3])

¿Qué señales indican que mis microservicios no compensan?

Guardias pesadas, bugs que tardan días en aislarse, coordinación constante para cambios pequeños, coste cloud subiendo sin mejora clara para el usuario.

¿Cómo mantengo autonomía de equipo sin microservicios?

Ownership por módulo, contratos claros, revisiones por owners, reglas automáticas de dependencias y tooling que haga visible el límite.

¿Event-driven architecture encaja en un monolito modular?

Sí. Puedes empezar con eventos in-process. Si un flujo pide resiliencia y reintentos, pasas ese caso a una cola real. Selectivo, no por moda.


Cierre

Si hoy estás valorando volver de microservicios a un monolito modular, no lo mires como una marcha atrás. Míralo como madurez. La arquitectura es un medio, no un trofeo.

Mi regla práctica es simple: paga complejidad solo cuando compre valor. Si la complejidad te compra “opciones futuras” pero te quita velocidad hoy, tu producto lo notará antes que tu diagrama.